Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Multi data source] Add interfaces to register add-on authentication method from plug-in module #5851

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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', () => {
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
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"`);
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
});

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() {
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
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>
bandinib-amzn marked this conversation as resolved.
Show resolved Hide resolved
) => {
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
Loading