From fa981f7810df4f922bee6027149e841a2c88e7c6 Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Thu, 8 Feb 2024 21:25:01 +0000 Subject: [PATCH 1/6] Adds method to register credential provider during data source plugin setup Signed-off-by: Bandini Bhopi --- .../authentication_methods_registry.ts | 33 +++++++++++++++ .../data_source/server/auth_registry/index.ts | 9 ++++ src/plugins/data_source/server/plugin.ts | 41 +++++++++++++++---- .../server/routes/test_connection.ts | 4 +- src/plugins/data_source/server/types.ts | 31 ++++++++++++-- 5 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts create mode 100644 src/plugins/data_source/server/auth_registry/index.ts diff --git a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts new file mode 100644 index 000000000000..8e4f9fd3a9ca --- /dev/null +++ b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthMethodValues } from '../../server/types'; + +export type IAuthenticationMethodRegistery = Omit< + AuthenticationMethodRegistery, + 'registerAuthenticationMethod' +>; + +export class AuthenticationMethodRegistery { + private readonly authMethods = new Map(); + /** + * Register a authMethods with function to return credentials inside the registry. + * Authentication Method can only be registered once. subsequent calls with the same method name will throw an error. + */ + public registerAuthenticationMethod(name: string, authMethodValues: AuthMethodValues) { + if (this.authMethods.has(name)) { + throw new Error(`Authentication method '${name}' is already registered`); + } + this.authMethods.set(name, authMethodValues); + } + + public getAllAuthenticationMethods() { + return [...this.authMethods.values()]; + } + + public getAuthenticationMethod(name: string) { + return this.authMethods.get(name); + } +} diff --git a/src/plugins/data_source/server/auth_registry/index.ts b/src/plugins/data_source/server/auth_registry/index.ts new file mode 100644 index 000000000000..9352afd8b661 --- /dev/null +++ b/src/plugins/data_source/server/auth_registry/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { + IAuthenticationMethodRegistery, + AuthenticationMethodRegistery, +} from './authentication_methods_registry'; diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index eee9bc0b8e0e..0f4e1e61631f 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -23,19 +23,22 @@ import { LoggingAuditor } from './audit/logging_auditor'; import { CryptographyService, CryptographyServiceSetup } from './cryptography_service'; import { DataSourceService, DataSourceServiceSetup } from './data_source_service'; import { DataSourceSavedObjectsClientWrapper, dataSource } from './saved_objects'; -import { DataSourcePluginSetup, DataSourcePluginStart } from './types'; +import { AuthMethodValues, DataSourcePluginSetup, DataSourcePluginStart } from './types'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { ensureRawRequest } from '../../../../src/core/server/http/router'; import { createDataSourceError } from './lib/error'; import { registerTestConnectionRoute } from './routes/test_connection'; +import { AuthenticationMethodRegistery, IAuthenticationMethodRegistery } from './auth_registry'; export class DataSourcePlugin implements Plugin { private readonly logger: Logger; private readonly cryptographyService: CryptographyService; private readonly dataSourceService: DataSourceService; private readonly config$: Observable; + private started = false; + private authMethodsRegistry = new AuthenticationMethodRegistery(); constructor(private initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); @@ -44,7 +47,7 @@ export class DataSourcePlugin implements Plugin(); } - public async setup(core: CoreSetup) { + public async setup(core: CoreSetup) { this.logger.debug('dataSource: Setup'); // Register data source saved object type @@ -95,6 +98,12 @@ export class DataSourcePlugin implements Plugin coreStart.auditTrail); const dataSourceService: DataSourceServiceSetup = await this.dataSourceService.setup(config); + + const authRegistryPromise = core.getStartServices().then(([, , selfStart]) => { + const dataSourcePluginStart = selfStart as DataSourcePluginStart; + return dataSourcePluginStart.getAuthenticationMethodRegistery(); + }); + // Register data source plugin context to route handler context core.http.registerRouteHandlerContext( 'dataSource', @@ -102,24 +111,41 @@ export class DataSourcePlugin implements Plugin { + this.logger.debug(`Registered Credential Provider for authType = ${name}`); + if (this.started) { + throw new Error('cannot call `registerCredentialProvider` after service startup.'); + } + this.authMethodsRegistry.registerAuthenticationMethod(name, authMethodValues); + }; return { createDataSourceError: (e: any) => createDataSourceError(e), dataSourceEnabled: () => config.enabled, defaultClusterEnabled: () => config.defaultCluster, + registerCredentialProvider, }; } public start(core: CoreStart) { this.logger.debug('dataSource: Started'); - - return {}; + this.started = true; + return { + getAuthenticationMethodRegistery: () => this.authMethodsRegistry, + }; } public stop() { @@ -130,7 +156,8 @@ export class DataSourcePlugin implements Plugin + auditTrailPromise: Promise, + authRegistryPromise: Promise ): IContextProvider, 'dataSource'> => { return (context, req) => { return { diff --git a/src/plugins/data_source/server/routes/test_connection.ts b/src/plugins/data_source/server/routes/test_connection.ts index cba42517e535..85eea97c933c 100644 --- a/src/plugins/data_source/server/routes/test_connection.ts +++ b/src/plugins/data_source/server/routes/test_connection.ts @@ -9,11 +9,13 @@ import { AuthType, DataSourceAttributes, SigV4ServiceName } from '../../common/d import { DataSourceConnectionValidator } from './data_source_connection_validator'; import { DataSourceServiceSetup } from '../data_source_service'; import { CryptographyServiceSetup } from '../cryptography_service'; +import { IAuthenticationMethodRegistery } from '../auth_registry'; export const registerTestConnectionRoute = ( router: IRouter, dataSourceServiceSetup: DataSourceServiceSetup, - cryptography: CryptographyServiceSetup + cryptography: CryptographyServiceSetup, + authRegistryPromise: Promise ) => { router.post( { diff --git a/src/plugins/data_source/server/types.ts b/src/plugins/data_source/server/types.ts index eed435b57301..54d71562dfe4 100644 --- a/src/plugins/data_source/server/types.ts +++ b/src/plugins/data_source/server/types.ts @@ -7,11 +7,18 @@ import { LegacyCallAPIOptions, OpenSearchClient, SavedObjectsClientContract, + OpenSearchDashboardsRequest, } from 'src/core/server'; -import { DataSourceAttributes } from '../common/data_sources'; +import { + DataSourceAttributes, + AuthType, + UsernamePasswordTypedContent, + SigV4Content, +} from '../common/data_sources'; import { CryptographyServiceSetup } from './cryptography_service'; import { DataSourceError } from './lib/error'; +import { IAuthenticationMethodRegistery } from './auth_registry'; export interface LegacyClientCallAPIParams { endpoint: string; @@ -29,6 +36,21 @@ export interface DataSourceClientParams { testClientDataSourceAttr?: DataSourceAttributes; } +export interface DataSourceCredentialsProviderOptions { + dataSourceAttr: DataSourceAttributes; + request?: OpenSearchDashboardsRequest; + cryptography?: CryptographyServiceSetup; +} + +export type DataSourceCredentialsProvider = ( + options: DataSourceCredentialsProviderOptions +) => Promise; + +export interface AuthMethodValues { + credentialProvider: DataSourceCredentialsProvider; + authType: AuthType; +} + export interface DataSourcePluginRequestContext { opensearch: { getClient: (dataSourceId: string) => Promise; @@ -55,6 +77,9 @@ export interface DataSourcePluginSetup { createDataSourceError: (err: any) => DataSourceError; dataSourceEnabled: () => boolean; defaultClusterEnabled: () => boolean; + registerCredentialProvider: (name: string, authMethodValues: AuthMethodValues) => void; +} + +export interface DataSourcePluginStart { + getAuthenticationMethodRegistery: () => IAuthenticationMethodRegistery; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataSourcePluginStart {} From c80df0b6b9c0b4fb40e389f4c3b228eacd111699 Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Thu, 8 Feb 2024 21:58:47 +0000 Subject: [PATCH 2/6] Adds method to register authentication method with UI elements during data source management plugin setup Signed-off-by: Bandini Bhopi --- .../data_source/common/data_sources/types.ts | 6 ++- .../authentication_methods_registry.ts | 40 ++++++++++++++++ .../public/auth_registry/index.ts | 10 ++++ .../data_source_management/public/index.ts | 1 + .../data_source_management/public/plugin.ts | 46 +++++++++++++++++-- 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts create mode 100644 src/plugins/data_source_management/public/auth_registry/index.ts diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index 8763c5306c15..d30e5ee710c8 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -11,11 +11,15 @@ export interface DataSourceAttributes extends SavedObjectAttributes { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + credentials: UsernamePasswordTypedContent | SigV4Content | undefined | AuthTypeContent; }; lastUpdatedTime?: string; } +export interface AuthTypeContent { + [key: string]: string; +} + /** * Multiple datasource supports authenticating as IAM user, it doesn't support IAM role. * Because IAM role session requires temporary security credentials through assuming role, diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts new file mode 100644 index 000000000000..6f8e81ae9ec1 --- /dev/null +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiSuperSelectOption } from '@elastic/eui'; +import { AuthTypeContent } from 'src/plugins/data_source/common/data_sources'; + +export interface AuthMethodUIElements { + credentialForm: React.JSX.Element; + credentialSourceOption: EuiSuperSelectOption; + credentialsFormValues: AuthTypeContent; +} + +export type IAuthenticationMethodRegistery = Omit< + AuthenticationMethodRegistery, + 'registerAuthenticationMethod' +>; + +export class AuthenticationMethodRegistery { + private readonly authMethods = new Map(); + /** + * Register a authMethods with function to return credentials inside the registry. + * Authentication Method can only be registered once. subsequent calls with the same method name will throw an error. + */ + public registerAuthenticationMethod(name: string, authMethodUIElements: AuthMethodUIElements) { + if (this.authMethods.has(name)) { + throw new Error(`Authentication method '${name}' is already registered`); + } + this.authMethods.set(name, authMethodUIElements); + } + + public getAllAuthenticationMethods() { + return [...this.authMethods.values()]; + } + + public getAuthenticationMethod(name: string) { + return this.authMethods.get(name); + } +} diff --git a/src/plugins/data_source_management/public/auth_registry/index.ts b/src/plugins/data_source_management/public/auth_registry/index.ts new file mode 100644 index 000000000000..f731b6528395 --- /dev/null +++ b/src/plugins/data_source_management/public/auth_registry/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { + IAuthenticationMethodRegistery, + AuthMethodUIElements, + AuthenticationMethodRegistery, +} from './authentication_methods_registry'; diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts index 5cd2c6c96d11..e58e8f9bce5e 100644 --- a/src/plugins/data_source_management/public/index.ts +++ b/src/plugins/data_source_management/public/index.ts @@ -12,3 +12,4 @@ export function plugin() { } export { DataSourceManagementPluginStart } from './types'; export { ClusterSelector } from './components/cluster_selector'; +export { DataSourceManagementPlugin, DataSourceManagementPluginSetup } from './plugin'; diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index 941107d74638..c915e6d79d14 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -10,18 +10,39 @@ import { PLUGIN_NAME } from '../common'; import { ManagementSetup } from '../../management/public'; import { IndexPatternManagementSetup } from '../../index_pattern_management/public'; import { DataSourceColumn } from './components/data_source_column/data_source_column'; +import { + AuthMethodUIElements, + IAuthenticationMethodRegistery, + AuthenticationMethodRegistery, +} from './auth_registry'; export interface DataSourceManagementSetupDependencies { management: ManagementSetup; indexPatternManagement: IndexPatternManagementSetup; } +export interface DataSourceManagementPluginSetup { + registerAuthenticationMethod: (name: string, authMethodValues: AuthMethodUIElements) => void; +} + +export interface DataSourceManagementPluginStart { + getAuthenticationMethodRegistery: () => IAuthenticationMethodRegistery; +} + const DSM_APP_ID = 'dataSources'; export class DataSourceManagementPlugin - implements Plugin { + implements + Plugin< + DataSourceManagementPluginSetup, + DataSourceManagementPluginStart, + DataSourceManagementSetupDependencies + > { + private started = false; + private authMethodsRegistry = new AuthenticationMethodRegistery(); + public setup( - core: CoreSetup, + core: CoreSetup, { management, indexPatternManagement }: DataSourceManagementSetupDependencies ) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; @@ -47,9 +68,28 @@ export class DataSourceManagementPlugin return mountManagementSection(core.getStartServices, params); }, }); + + const registerAuthenticationMethod = ( + name: string, + authMethodUIElements: AuthMethodUIElements + ) => { + if (this.started) { + throw new Error( + 'cannot call `registerAuthenticationMethod` after data source management startup.' + ); + } + this.authMethodsRegistry.registerAuthenticationMethod(name, authMethodUIElements); + }; + + return { registerAuthenticationMethod }; } - public start(core: CoreStart) {} + public start(core: CoreStart) { + this.started = true; + return { + getAuthenticationMethodRegistery: () => this.authMethodsRegistry, + }; + } public stop() {} } From f52481ffecb846c9c4813ed03ac175e0e4d12332 Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Fri, 9 Feb 2024 00:58:50 +0000 Subject: [PATCH 3/6] Adds UT for auth registry in data source plugin Signed-off-by: Bandini Bhopi --- .../authentication_methods_registry.test.ts | 106 ++++++++++++++++++ .../authentication_methods_registry.ts | 13 ++- src/plugins/data_source/server/plugin.ts | 8 +- src/plugins/data_source/server/types.ts | 7 +- 4 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts diff --git a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts new file mode 100644 index 000000000000..fa589321d29f --- /dev/null +++ b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthenticationMethodRegistery } from './authentication_methods_registry'; +import { AuthMethodType } from '../../server/types'; +import { AuthType } from '../../common/data_sources'; + +const createAuthenticationMethod = (authMethod: Partial): AuthMethodType => ({ + name: 'unknown', + authType: AuthType.NoAuth, + credentialProvider: jest.fn(), + ...authMethod, +}); + +describe('AuthenticationMethodRegistery', () => { + let registry: AuthenticationMethodRegistery; + + beforeEach(() => { + registry = new AuthenticationMethodRegistery(); + }); + + it('allows to register authentication method', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + expect( + registry + .getAllAuthenticationMethods() + .map((type) => type.name) + .sort() + ).toEqual(['typeA', 'typeB', 'typeC']); + }); + + it('throws when trying to register the same authentication method twice', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + expect(() => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + }).toThrowErrorMatchingInlineSnapshot(`"Authentication method 'typeA' is already registered"`); + }); + + describe('#getAuthenticationMethod', () => { + it(`retrieve a type by it's name`, () => { + const typeA = createAuthenticationMethod({ name: 'typeA' }); + const typeB = createAuthenticationMethod({ name: 'typeB' }); + registry.registerAuthenticationMethod(typeA); + registry.registerAuthenticationMethod(typeB); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + expect(registry.getAuthenticationMethod('typeA')).toEqual(typeA); + expect(registry.getAuthenticationMethod('typeB')).toEqual(typeB); + expect(registry.getAuthenticationMethod('unknownType')).toBeUndefined(); + }); + + it('forbids to mutate the registered types', () => { + registry.registerAuthenticationMethod( + createAuthenticationMethod({ + name: 'typeA', + authType: AuthType.NoAuth, + }) + ); + + const typeA = registry.getAuthenticationMethod('typeA')!; + + expect(() => { + typeA.authType = AuthType.SigV4; + }).toThrow(); + expect(() => { + typeA.name = 'foo'; + }).toThrow(); + expect(() => { + typeA.credentialProvider = jest.fn(); + }).toThrow(); + }); + }); + + describe('#getAllTypes', () => { + it('returns all registered types', () => { + const typeA = createAuthenticationMethod({ name: 'typeA' }); + const typeB = createAuthenticationMethod({ name: 'typeB' }); + const typeC = createAuthenticationMethod({ name: 'typeC' }); + registry.registerAuthenticationMethod(typeA); + registry.registerAuthenticationMethod(typeB); + + const registered = registry.getAllAuthenticationMethods(); + expect(registered.length).toEqual(2); + expect(registered).toContainEqual(typeA); + expect(registered).toContainEqual(typeB); + expect(registered).not.toContainEqual(typeC); + }); + + it('does not mutate the registered types when altering the list', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + const types = registry.getAllAuthenticationMethods(); + types.splice(0, 3); + + expect(registry.getAllAuthenticationMethods().length).toEqual(3); + }); + }); +}); diff --git a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts index 8e4f9fd3a9ca..329ee0c65906 100644 --- a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts +++ b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthMethodValues } from '../../server/types'; +import { deepFreeze } from '@osd/std'; +import { AuthMethodType } from '../../server/types'; export type IAuthenticationMethodRegistery = Omit< AuthenticationMethodRegistery, @@ -11,16 +12,16 @@ export type IAuthenticationMethodRegistery = Omit< >; export class AuthenticationMethodRegistery { - private readonly authMethods = new Map(); + private readonly authMethods = new Map(); /** * Register a authMethods with function to return credentials inside the registry. * Authentication Method can only be registered once. subsequent calls with the same method name will throw an error. */ - public registerAuthenticationMethod(name: string, authMethodValues: AuthMethodValues) { - if (this.authMethods.has(name)) { - throw new Error(`Authentication method '${name}' is already registered`); + public registerAuthenticationMethod(method: AuthMethodType) { + if (this.authMethods.has(method.name)) { + throw new Error(`Authentication method '${method.name}' is already registered`); } - this.authMethods.set(name, authMethodValues); + this.authMethods.set(method.name, deepFreeze(method) as AuthMethodType); } public getAllAuthenticationMethods() { diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index 0f4e1e61631f..6afa378c5a0f 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -23,7 +23,7 @@ import { LoggingAuditor } from './audit/logging_auditor'; import { CryptographyService, CryptographyServiceSetup } from './cryptography_service'; import { DataSourceService, DataSourceServiceSetup } from './data_source_service'; import { DataSourceSavedObjectsClientWrapper, dataSource } from './saved_objects'; -import { AuthMethodValues, DataSourcePluginSetup, DataSourcePluginStart } from './types'; +import { AuthMethodType, DataSourcePluginSetup, DataSourcePluginStart } from './types'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common'; // eslint-disable-next-line @osd/eslint/no-restricted-paths @@ -124,12 +124,12 @@ export class DataSourcePlugin implements Plugin { - this.logger.debug(`Registered Credential Provider for authType = ${name}`); + const registerCredentialProvider = (method: AuthMethodType) => { + this.logger.debug(`Registered Credential Provider for authType = ${method.name}`); if (this.started) { throw new Error('cannot call `registerCredentialProvider` after service startup.'); } - this.authMethodsRegistry.registerAuthenticationMethod(name, authMethodValues); + this.authMethodsRegistry.registerAuthenticationMethod(method); }; return { diff --git a/src/plugins/data_source/server/types.ts b/src/plugins/data_source/server/types.ts index 54d71562dfe4..a97c1fc85368 100644 --- a/src/plugins/data_source/server/types.ts +++ b/src/plugins/data_source/server/types.ts @@ -46,9 +46,10 @@ export type DataSourceCredentialsProvider = ( options: DataSourceCredentialsProviderOptions ) => Promise; -export interface AuthMethodValues { - credentialProvider: DataSourceCredentialsProvider; +export interface AuthMethodType { + name: string; authType: AuthType; + credentialProvider: DataSourceCredentialsProvider; } export interface DataSourcePluginRequestContext { @@ -77,7 +78,7 @@ export interface DataSourcePluginSetup { createDataSourceError: (err: any) => DataSourceError; dataSourceEnabled: () => boolean; defaultClusterEnabled: () => boolean; - registerCredentialProvider: (name: string, authMethodValues: AuthMethodValues) => void; + registerCredentialProvider: (method: AuthMethodType) => void; } export interface DataSourcePluginStart { From bf4eebe630bcd13ed6b8b0812710b671b5f85213 Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Fri, 9 Feb 2024 23:23:29 +0000 Subject: [PATCH 4/6] Adds UT for auth registry in data source management plugin Signed-off-by: Bandini Bhopi --- CHANGELOG.md | 1 + .../authentication_methods_registry.test.ts | 6 +- .../authentication_methods_registry.ts | 8 +- src/plugins/data_source/server/plugin.ts | 4 +- src/plugins/data_source/server/types.ts | 4 +- .../authentication_methods_registry.test.ts | 110 ++++++++++++++++++ .../authentication_methods_registry.ts | 16 +-- .../public/auth_registry/index.ts | 2 +- .../data_source_management/public/plugin.ts | 11 +- 9 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee4f091d3d71..ac6c30e58d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Discover] Enhanced the data source selector with added sorting functionality ([#5609](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5609)) - [Multiple Datasource] Add datasource picker component and use it in devtools and tutorial page when multiple datasource is enabled ([#5756](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5756)) - [Multiple Datasource] Add datasource picker to import saved object flyout when multiple data source is enabled ([#5781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5781)) +- [Multiple Datasource] Add interfaces to register add-on authentication method from plug-in module ([#5851](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5851)) ### 🐛 Bug Fixes diff --git a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts index fa589321d29f..c7692acee782 100644 --- a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts +++ b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.test.ts @@ -4,10 +4,12 @@ */ import { AuthenticationMethodRegistery } from './authentication_methods_registry'; -import { AuthMethodType } from '../../server/types'; +import { AuthenticationMethod } from '../../server/types'; import { AuthType } from '../../common/data_sources'; -const createAuthenticationMethod = (authMethod: Partial): AuthMethodType => ({ +const createAuthenticationMethod = ( + authMethod: Partial +): AuthenticationMethod => ({ name: 'unknown', authType: AuthType.NoAuth, credentialProvider: jest.fn(), diff --git a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts index 329ee0c65906..e2f39498e007 100644 --- a/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts +++ b/src/plugins/data_source/server/auth_registry/authentication_methods_registry.ts @@ -4,7 +4,7 @@ */ import { deepFreeze } from '@osd/std'; -import { AuthMethodType } from '../../server/types'; +import { AuthenticationMethod } from '../../server/types'; export type IAuthenticationMethodRegistery = Omit< AuthenticationMethodRegistery, @@ -12,16 +12,16 @@ export type IAuthenticationMethodRegistery = Omit< >; export class AuthenticationMethodRegistery { - private readonly authMethods = new Map(); + private readonly authMethods = new Map(); /** * Register a authMethods with function to return credentials inside the registry. * Authentication Method can only be registered once. subsequent calls with the same method name will throw an error. */ - public registerAuthenticationMethod(method: AuthMethodType) { + public registerAuthenticationMethod(method: AuthenticationMethod) { if (this.authMethods.has(method.name)) { throw new Error(`Authentication method '${method.name}' is already registered`); } - this.authMethods.set(method.name, deepFreeze(method) as AuthMethodType); + this.authMethods.set(method.name, deepFreeze(method) as AuthenticationMethod); } public getAllAuthenticationMethods() { diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index 6afa378c5a0f..1a90d22960e5 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -23,7 +23,7 @@ import { LoggingAuditor } from './audit/logging_auditor'; import { CryptographyService, CryptographyServiceSetup } from './cryptography_service'; import { DataSourceService, DataSourceServiceSetup } from './data_source_service'; import { DataSourceSavedObjectsClientWrapper, dataSource } from './saved_objects'; -import { AuthMethodType, DataSourcePluginSetup, DataSourcePluginStart } from './types'; +import { AuthenticationMethod, DataSourcePluginSetup, DataSourcePluginStart } from './types'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common'; // eslint-disable-next-line @osd/eslint/no-restricted-paths @@ -124,7 +124,7 @@ export class DataSourcePlugin implements Plugin { + const registerCredentialProvider = (method: AuthenticationMethod) => { this.logger.debug(`Registered Credential Provider for authType = ${method.name}`); if (this.started) { throw new Error('cannot call `registerCredentialProvider` after service startup.'); diff --git a/src/plugins/data_source/server/types.ts b/src/plugins/data_source/server/types.ts index a97c1fc85368..b54eb5db0eb9 100644 --- a/src/plugins/data_source/server/types.ts +++ b/src/plugins/data_source/server/types.ts @@ -46,7 +46,7 @@ export type DataSourceCredentialsProvider = ( options: DataSourceCredentialsProviderOptions ) => Promise; -export interface AuthMethodType { +export interface AuthenticationMethod { name: string; authType: AuthType; credentialProvider: DataSourceCredentialsProvider; @@ -78,7 +78,7 @@ export interface DataSourcePluginSetup { createDataSourceError: (err: any) => DataSourceError; dataSourceEnabled: () => boolean; defaultClusterEnabled: () => boolean; - registerCredentialProvider: (method: AuthMethodType) => void; + registerCredentialProvider: (method: AuthenticationMethod) => void; } export interface DataSourcePluginStart { diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts new file mode 100644 index 000000000000..b98cc4cefeed --- /dev/null +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + AuthenticationMethodRegistery, + AuthenticationMethod, +} from './authentication_methods_registry'; +import React from 'react'; + +const createAuthenticationMethod = ( + authMethod: Partial +): AuthenticationMethod => ({ + name: 'unknown', + credentialForm: React.createElement('div', {}, 'Hello, world!'), + credentialSourceOption: { + value: 'unknown', + }, + ...authMethod, +}); + +describe('AuthenticationMethodRegistery', () => { + let registry: AuthenticationMethodRegistery; + + beforeEach(() => { + registry = new AuthenticationMethodRegistery(); + }); + + it('allows to register authentication method', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + expect( + registry + .getAllAuthenticationMethods() + .map((type) => type.name) + .sort() + ).toEqual(['typeA', 'typeB', 'typeC']); + }); + + it('throws when trying to register the same authentication method twice', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + expect(() => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + }).toThrowErrorMatchingInlineSnapshot(`"Authentication method 'typeA' is already registered"`); + }); + + describe('#getAuthenticationMethod', () => { + it(`retrieve a type by it's name`, () => { + const typeA = createAuthenticationMethod({ name: 'typeA' }); + const typeB = createAuthenticationMethod({ name: 'typeB' }); + registry.registerAuthenticationMethod(typeA); + registry.registerAuthenticationMethod(typeB); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + expect(registry.getAuthenticationMethod('typeA')).toEqual(typeA); + expect(registry.getAuthenticationMethod('typeB')).toEqual(typeB); + expect(registry.getAuthenticationMethod('unknownType')).toBeUndefined(); + }); + + it('forbids to mutate the registered types', () => { + registry.registerAuthenticationMethod( + createAuthenticationMethod({ + name: 'typeA', + }) + ); + + const typeA = registry.getAuthenticationMethod('typeA')!; + + expect(() => { + typeA.credentialForm = React.createElement('div', {}, 'Welcome!'); + }).toThrow(); + expect(() => { + typeA.credentialSourceOption = { + value: 'typeA', + }; + }).toThrow(); + }); + }); + + describe('#getAllTypes', () => { + it('returns all registered types', () => { + const typeA = createAuthenticationMethod({ name: 'typeA' }); + const typeB = createAuthenticationMethod({ name: 'typeB' }); + const typeC = createAuthenticationMethod({ name: 'typeC' }); + registry.registerAuthenticationMethod(typeA); + registry.registerAuthenticationMethod(typeB); + + const registered = registry.getAllAuthenticationMethods(); + expect(registered.length).toEqual(2); + expect(registered).toContainEqual(typeA); + expect(registered).toContainEqual(typeB); + expect(registered).not.toContainEqual(typeC); + }); + + it('does not mutate the registered types when altering the list', () => { + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeA' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeB' })); + registry.registerAuthenticationMethod(createAuthenticationMethod({ name: 'typeC' })); + + const types = registry.getAllAuthenticationMethods(); + types.splice(0, 3); + + expect(registry.getAllAuthenticationMethods().length).toEqual(3); + }); + }); +}); diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts index 6f8e81ae9ec1..98cff913483f 100644 --- a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { deepFreeze } from '@osd/std'; import { EuiSuperSelectOption } from '@elastic/eui'; -import { AuthTypeContent } from 'src/plugins/data_source/common/data_sources'; -export interface AuthMethodUIElements { +export interface AuthenticationMethod { + name: string; credentialForm: React.JSX.Element; credentialSourceOption: EuiSuperSelectOption; - credentialsFormValues: AuthTypeContent; } export type IAuthenticationMethodRegistery = Omit< @@ -18,16 +18,16 @@ export type IAuthenticationMethodRegistery = Omit< >; export class AuthenticationMethodRegistery { - private readonly authMethods = new Map(); + private readonly authMethods = new Map(); /** * Register a authMethods with function to return credentials inside the registry. * Authentication Method can only be registered once. subsequent calls with the same method name will throw an error. */ - public registerAuthenticationMethod(name: string, authMethodUIElements: AuthMethodUIElements) { - if (this.authMethods.has(name)) { - throw new Error(`Authentication method '${name}' is already registered`); + public registerAuthenticationMethod(method: AuthenticationMethod) { + if (this.authMethods.has(method.name)) { + throw new Error(`Authentication method '${method.name}' is already registered`); } - this.authMethods.set(name, authMethodUIElements); + this.authMethods.set(method.name, deepFreeze(method) as AuthenticationMethod); } public getAllAuthenticationMethods() { diff --git a/src/plugins/data_source_management/public/auth_registry/index.ts b/src/plugins/data_source_management/public/auth_registry/index.ts index f731b6528395..5cbadd12a51a 100644 --- a/src/plugins/data_source_management/public/auth_registry/index.ts +++ b/src/plugins/data_source_management/public/auth_registry/index.ts @@ -5,6 +5,6 @@ export { IAuthenticationMethodRegistery, - AuthMethodUIElements, + AuthenticationMethod, AuthenticationMethodRegistery, } from './authentication_methods_registry'; diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index c915e6d79d14..0c7123e47a94 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -11,7 +11,7 @@ import { ManagementSetup } from '../../management/public'; import { IndexPatternManagementSetup } from '../../index_pattern_management/public'; import { DataSourceColumn } from './components/data_source_column/data_source_column'; import { - AuthMethodUIElements, + AuthenticationMethod, IAuthenticationMethodRegistery, AuthenticationMethodRegistery, } from './auth_registry'; @@ -22,7 +22,7 @@ export interface DataSourceManagementSetupDependencies { } export interface DataSourceManagementPluginSetup { - registerAuthenticationMethod: (name: string, authMethodValues: AuthMethodUIElements) => void; + registerAuthenticationMethod: (authMethodValues: AuthenticationMethod) => void; } export interface DataSourceManagementPluginStart { @@ -69,16 +69,13 @@ export class DataSourceManagementPlugin }, }); - const registerAuthenticationMethod = ( - name: string, - authMethodUIElements: AuthMethodUIElements - ) => { + const registerAuthenticationMethod = (authMethod: AuthenticationMethod) => { if (this.started) { throw new Error( 'cannot call `registerAuthenticationMethod` after data source management startup.' ); } - this.authMethodsRegistry.registerAuthenticationMethod(name, authMethodUIElements); + this.authMethodsRegistry.registerAuthenticationMethod(authMethod); }; return { registerAuthenticationMethod }; From 930eb553d9c5f6d1c341a38330ccfc5c921db58e Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Tue, 13 Feb 2024 00:36:33 +0000 Subject: [PATCH 5/6] Adds UT for data_source_management plugin.ts Signed-off-by: Bandini Bhopi --- .../authentication_methods_registry.test.ts | 2 +- .../public/auth_registry/index.ts | 2 ++ .../data_source_management/public/mocks.ts | 28 +++++++++++++++++++ .../public/plugin.test.ts | 26 +++++++++++++++++ .../index_pattern_management/public/mocks.ts | 3 ++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data_source_management/public/plugin.test.ts diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts index b98cc4cefeed..d362f3b72ae8 100644 --- a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts @@ -9,7 +9,7 @@ import { } from './authentication_methods_registry'; import React from 'react'; -const createAuthenticationMethod = ( +export const createAuthenticationMethod = ( authMethod: Partial ): AuthenticationMethod => ({ name: 'unknown', diff --git a/src/plugins/data_source_management/public/auth_registry/index.ts b/src/plugins/data_source_management/public/auth_registry/index.ts index 5cbadd12a51a..3892f08e0c4c 100644 --- a/src/plugins/data_source_management/public/auth_registry/index.ts +++ b/src/plugins/data_source_management/public/auth_registry/index.ts @@ -8,3 +8,5 @@ export { AuthenticationMethod, AuthenticationMethodRegistery, } from './authentication_methods_registry'; + +export { createAuthenticationMethod } from './authentication_methods_registry.test'; diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index c078247956e0..cdf86b9b1b08 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -7,6 +7,13 @@ import { throwError } from 'rxjs'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { AuthType } from './types'; import { coreMock } from '../../../core/public/mocks'; +import { + DataSourceManagementPlugin, + DataSourceManagementPluginSetup, + DataSourceManagementPluginStart, +} from './plugin'; +import { managementPluginMock } from '../../management/public/mocks'; +import { mockManagementPlugin as indexPatternManagementPluginMock } from '../../index_pattern_management/public/mocks'; /* Mock Types */ @@ -197,3 +204,24 @@ export const mockErrorResponseForSavedObjectsCalls = ( throwError(new Error('Error while fetching data sources')) ); }; + +export interface TestPluginReturn { + setup: DataSourceManagementPluginSetup; + doStart: () => DataSourceManagementPluginStart; +} + +export const testDataSourceManagementPlugin = ( + coreSetup: any, + coreStart: any +): TestPluginReturn => { + const plugin = new DataSourceManagementPlugin(); + const setup = plugin.setup(coreSetup, { + management: managementPluginMock.createSetupContract(), + indexPatternManagement: indexPatternManagementPluginMock.createSetupContract(), + }); + const doStart = () => { + const start = plugin.start(coreStart); + return start; + }; + return { setup, doStart }; +}; diff --git a/src/plugins/data_source_management/public/plugin.test.ts b/src/plugins/data_source_management/public/plugin.test.ts new file mode 100644 index 000000000000..893c25d57604 --- /dev/null +++ b/src/plugins/data_source_management/public/plugin.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { coreMock } from '../../../core/public/mocks'; +import { DataSourceManagementPluginStart } from './plugin'; +import { testDataSourceManagementPlugin } from './mocks'; +import { createAuthenticationMethod } from './auth_registry'; + +describe('#dataSourceManagement', () => { + let coreSetup: any; + let coreStart: any; + let mockDataSourceManagementPluginStart: MockedKeys; + beforeEach(() => { + coreSetup = coreMock.createSetup({ pluginStartContract: mockDataSourceManagementPluginStart }); + coreStart = coreMock.createStart(); + }); + it('can register custom authentication method', () => { + const { setup, doStart } = testDataSourceManagementPlugin(coreSetup, coreStart); + const typeA = createAuthenticationMethod({ name: 'typeA' }); + setup.registerAuthenticationMethod(createAuthenticationMethod(typeA)); + const start = doStart(); + const registry = start.getAuthenticationMethodRegistery(); + expect(registry.getAuthenticationMethod('typeA')).toEqual(typeA); + }); +}); diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 295bd7e3faee..dacf876c2f6c 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -53,6 +53,9 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ environment: { update: jest.fn(), }, + columns: { + register: jest.fn(), + }, }); const createStartContract = (): IndexPatternManagementStart => ({ From 6933ea063688246ee873bb5d63f500a459d28a13 Mon Sep 17 00:00:00 2001 From: Bandini Bhopi Date: Tue, 13 Feb 2024 01:57:16 +0000 Subject: [PATCH 6/6] Refactor code Signed-off-by: Bandini Bhopi --- .../authentication_methods_registry.test.ts | 17 ++--------------- .../public/auth_registry/index.ts | 2 -- .../data_source_management/public/mocks.ts | 13 +++++++++++++ .../public/plugin.test.ts | 3 +-- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts index d362f3b72ae8..f2bd07af4dc5 100644 --- a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.test.ts @@ -3,22 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AuthenticationMethodRegistery, - AuthenticationMethod, -} from './authentication_methods_registry'; +import { AuthenticationMethodRegistery } from './authentication_methods_registry'; import React from 'react'; - -export const createAuthenticationMethod = ( - authMethod: Partial -): AuthenticationMethod => ({ - name: 'unknown', - credentialForm: React.createElement('div', {}, 'Hello, world!'), - credentialSourceOption: { - value: 'unknown', - }, - ...authMethod, -}); +import { createAuthenticationMethod } from '../mocks'; describe('AuthenticationMethodRegistery', () => { let registry: AuthenticationMethodRegistery; diff --git a/src/plugins/data_source_management/public/auth_registry/index.ts b/src/plugins/data_source_management/public/auth_registry/index.ts index 3892f08e0c4c..5cbadd12a51a 100644 --- a/src/plugins/data_source_management/public/auth_registry/index.ts +++ b/src/plugins/data_source_management/public/auth_registry/index.ts @@ -8,5 +8,3 @@ export { AuthenticationMethod, AuthenticationMethodRegistery, } from './authentication_methods_registry'; - -export { createAuthenticationMethod } from './authentication_methods_registry.test'; diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index cdf86b9b1b08..7b170c4a7c79 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; import { throwError } from 'rxjs'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { AuthType } from './types'; @@ -14,6 +15,7 @@ import { } from './plugin'; import { managementPluginMock } from '../../management/public/mocks'; import { mockManagementPlugin as indexPatternManagementPluginMock } from '../../index_pattern_management/public/mocks'; +import { AuthenticationMethod } from './auth_registry'; /* Mock Types */ @@ -225,3 +227,14 @@ export const testDataSourceManagementPlugin = ( }; return { setup, doStart }; }; + +export const createAuthenticationMethod = ( + authMethod: Partial +): AuthenticationMethod => ({ + name: 'unknown', + credentialForm: React.createElement('div', {}, 'Hello, world!'), + credentialSourceOption: { + value: 'unknown', + }, + ...authMethod, +}); diff --git a/src/plugins/data_source_management/public/plugin.test.ts b/src/plugins/data_source_management/public/plugin.test.ts index 893c25d57604..98615119a0c1 100644 --- a/src/plugins/data_source_management/public/plugin.test.ts +++ b/src/plugins/data_source_management/public/plugin.test.ts @@ -4,8 +4,7 @@ */ import { coreMock } from '../../../core/public/mocks'; import { DataSourceManagementPluginStart } from './plugin'; -import { testDataSourceManagementPlugin } from './mocks'; -import { createAuthenticationMethod } from './auth_registry'; +import { testDataSourceManagementPlugin, createAuthenticationMethod } from './mocks'; describe('#dataSourceManagement', () => { let coreSetup: any;