Skip to content

Commit

Permalink
[Multi data source] Add interfaces to register add-on authentication …
Browse files Browse the repository at this point in the history
…method from plug-in module (opensearch-project#5851)

* Adds method to register credential provider during data source plugin setup

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

* Adds method to register authentication method with UI elements during data source management plugin setup

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

* Adds UT for auth registry in data source plugin

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

* Adds UT for auth registry in data source management plugin

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

* Adds UT for data_source_management plugin.ts

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

* Refactor code

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>

---------

Signed-off-by: Bandini Bhopi <bandinib@amazon.com>
(cherry picked from commit e08bf30)
  • Loading branch information
bandinib-amzn committed Feb 20, 2024
1 parent 57200af commit 01d5856
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Custom Branding] Relative URL should be allowed for logos ([#5572](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5572))
- [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

Expand Down
6 changes: 5 additions & 1 deletion src/plugins/data_source/common/data_sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { AuthenticationMethodRegistery } from './authentication_methods_registry';
import { AuthenticationMethod } from '../../server/types';
import { AuthType } from '../../common/data_sources';

const createAuthenticationMethod = (
authMethod: Partial<AuthenticationMethod>
): AuthenticationMethod => ({
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);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { deepFreeze } from '@osd/std';
import { AuthenticationMethod } from '../../server/types';

export type IAuthenticationMethodRegistery = Omit<
AuthenticationMethodRegistery,
'registerAuthenticationMethod'
>;

export class AuthenticationMethodRegistery {
private readonly authMethods = new Map<string, AuthenticationMethod>();
/**
* 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: 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 AuthenticationMethod);
}

public getAllAuthenticationMethods() {
return [...this.authMethods.values()];
}

public getAuthenticationMethod(name: string) {
return this.authMethods.get(name);
}
}
9 changes: 9 additions & 0 deletions src/plugins/data_source/server/auth_registry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export {
IAuthenticationMethodRegistery,
AuthenticationMethodRegistery,
} from './authentication_methods_registry';
41 changes: 34 additions & 7 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { AuthenticationMethod, 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<DataSourcePluginSetup, DataSourcePluginStart> {
private readonly logger: Logger;
private readonly cryptographyService: CryptographyService;
private readonly dataSourceService: DataSourceService;
private readonly config$: Observable<DataSourcePluginConfigType>;
private started = false;
private authMethodsRegistry = new AuthenticationMethodRegistery();

constructor(private initializerContext: PluginInitializerContext<DataSourcePluginConfigType>) {
this.logger = this.initializerContext.logger.get();
Expand All @@ -44,7 +47,7 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
this.config$ = this.initializerContext.config.create<DataSourcePluginConfigType>();
}

public async setup(core: CoreSetup) {
public async setup(core: CoreSetup<DataSourcePluginStart>) {
this.logger.debug('dataSource: Setup');

// Register data source saved object type
Expand Down Expand Up @@ -95,31 +98,54 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
const auditTrailPromise = core.getStartServices().then(([coreStart]) => 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',
this.createDataSourceRouteHandlerContext(
dataSourceService,
cryptographyServiceSetup,
this.logger,
auditTrailPromise
auditTrailPromise,
authRegistryPromise
)
);

const router = core.http.createRouter();
registerTestConnectionRoute(router, dataSourceService, cryptographyServiceSetup);
registerTestConnectionRoute(
router,
dataSourceService,
cryptographyServiceSetup,
authRegistryPromise
);

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.');
}
this.authMethodsRegistry.registerAuthenticationMethod(method);
};

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() {
Expand All @@ -130,7 +156,8 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
dataSourceService: DataSourceServiceSetup,
cryptography: CryptographyServiceSetup,
logger: Logger,
auditTrailPromise: Promise<AuditorFactory>
auditTrailPromise: Promise<AuditorFactory>,
authRegistryPromise: Promise<IAuthenticationMethodRegistery>
): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'dataSource'> => {
return (context, req) => {
return {
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/data_source/server/routes/test_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAuthenticationMethodRegistery>
) => {
router.post(
{
Expand Down
32 changes: 29 additions & 3 deletions src/plugins/data_source/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +36,22 @@ export interface DataSourceClientParams {
testClientDataSourceAttr?: DataSourceAttributes;
}

export interface DataSourceCredentialsProviderOptions {
dataSourceAttr: DataSourceAttributes;
request?: OpenSearchDashboardsRequest;
cryptography?: CryptographyServiceSetup;
}

export type DataSourceCredentialsProvider = (
options: DataSourceCredentialsProviderOptions
) => Promise<UsernamePasswordTypedContent | SigV4Content>;

export interface AuthenticationMethod {
name: string;
authType: AuthType;
credentialProvider: DataSourceCredentialsProvider;
}

export interface DataSourcePluginRequestContext {
opensearch: {
getClient: (dataSourceId: string) => Promise<OpenSearchClient>;
Expand All @@ -55,6 +78,9 @@ export interface DataSourcePluginSetup {
createDataSourceError: (err: any) => DataSourceError;
dataSourceEnabled: () => boolean;
defaultClusterEnabled: () => boolean;
registerCredentialProvider: (method: AuthenticationMethod) => void;
}

export interface DataSourcePluginStart {
getAuthenticationMethodRegistery: () => IAuthenticationMethodRegistery;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DataSourcePluginStart {}
Loading

0 comments on commit 01d5856

Please sign in to comment.