From 20b6c5a9a145e44fef05be9c40fccbaa4a7d9189 Mon Sep 17 00:00:00 2001 From: Xinrui Bai-amazon <139305463+xinruiba@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:31:16 -0800 Subject: [PATCH] Hide/Show authentication method in multi data source plugin based on configuration (#5916) * [AuthType Config] Add EnabledAuthType configuration in yml file and pass value to dataSourceManagement root mount Signed-off-by: Xinrui Bai * [AuthType Config] provide default auth types when types array is empty Signed-off-by: Xinrui Bai * [AuthRegistry Onboard - DataSourceCreationForm] Integrate auth cofig with AuthRegistray and providing data source options from AuthRegistry Signed-off-by: Xinrui Bai * [AuthRegistry Onboard] Support default selected auth type in datasource creation page Signed-off-by: Xinrui Bai * [AuthRegistry Onboard] SDisable auth option selector when auth options no more than 1 Signed-off-by: Xinrui Bai * [AuthRegistry Onboard] clear credential attribute every time use select NoAuth option. Also update credentialField data type Signed-off-by: Xinrui Bai * Update yml file to disable config Signed-off-by: Xinrui Bai * Update change.md file Signed-off-by: Xinrui Bai * [UT] Fix broken test cases Signed-off-by: Xinrui Bai * [UT] Add more unit tests Signed-off-by: Xinrui Bai * [UT] update unit test and handle scenario when no options enabled Signed-off-by: Xinrui Bai * [UT] update description of unit test case Signed-off-by: Xinrui Bai * [UT] update unit test cases to resolve comment Signed-off-by: Xinrui Bai * [UT] Snapshot auth option super selector for unit test. Also upate the default message in datasorurce creation form ---> auth type section Signed-off-by: Xinrui Bai --------- Signed-off-by: Xinrui Bai --- CHANGELOG.md | 1 + config/opensearch_dashboards.yml | 11 + src/plugins/data_source/config.ts | 11 + src/plugins/data_source/public/plugin.ts | 6 + src/plugins/data_source/public/types.ts | 6 + src/plugins/data_source/server/index.ts | 1 + .../authentication_methods_registry.ts | 3 +- .../create_data_source_form.test.tsx.snap | 1657 +++++++++++++++++ .../create_data_source_form.test.tsx | 143 +- .../create_form/create_data_source_form.tsx | 49 +- .../edit_form/edit_data_source_form.test.tsx | 15 +- .../edit_form/edit_data_source_form.tsx | 14 +- .../edit_data_source.test.tsx | 13 + .../public/components/utils.test.ts | 55 +- .../public/components/utils.ts | 24 +- .../mount_management_section.tsx | 5 +- .../data_source_management/public/mocks.ts | 12 +- .../data_source_management/public/plugin.ts | 17 +- .../data_source_management/public/types.ts | 82 +- 19 files changed, 2081 insertions(+), 44 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index fd11ee4c10bd..7d62425341d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Concatenate data source name with index pattern name and change delimiter to double colon ([#5907](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5907)) - [Multiple Datasource] Refactor client and legacy client to use authentication registry ([#5881](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5881)) - [Multiple Datasource] Improved error handling for the search API when a null value is passed for the dataSourceId ([#5882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5882)) +- [Multiple Datasource] Hide/Show authentication method in multi data source plugin based on configuration ([#5916](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5916)) ### 🐛 Bug Fixes diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 9797335e3cce..4f86a8729a3e 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -270,6 +270,17 @@ # 'ff00::/8', # ] +# Set enabled false to hide authentication method in OpenSearch Dashboards. +# If this setting is commented then all 3 options will be available in OpenSearch Dashboards. +# Default value will be considered to True. +#data_source.authTypes: +# NoAuthentication: +# enabled: true +# UsernamePassword: +# enabled: true +# AWSSigV4: +# enabled: true + # Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey # opensearchDashboards.survey.url: "https://survey.opensearch.org" diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index d5412d32f0ff..50013537b127 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -39,6 +39,17 @@ export const configSchema = schema.object({ appender: fileAppenderSchema, }), endpointDeniedIPs: schema.maybe(schema.arrayOf(schema.string())), + authTypes: schema.object({ + NoAuthentication: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + UsernamePassword: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + AWSSigV4: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }), }); export type DataSourcePluginConfigType = TypeOf; diff --git a/src/plugins/data_source/public/plugin.ts b/src/plugins/data_source/public/plugin.ts index 65bee912255e..dd2f70a7c193 100644 --- a/src/plugins/data_source/public/plugin.ts +++ b/src/plugins/data_source/public/plugin.ts @@ -22,6 +22,9 @@ export class DataSourcePlugin implements Plugin = { exposeToBrowser: { enabled: true, hideLocalCluster: true, + authTypes: true, }, schema: configSchema, }; 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 98cff913483f..00c3b0dbf0ee 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 @@ -8,8 +8,9 @@ import { EuiSuperSelectOption } from '@elastic/eui'; export interface AuthenticationMethod { name: string; - credentialForm: React.JSX.Element; credentialSourceOption: EuiSuperSelectOption; + credentialForm?: React.JSX.Element; + crendentialFormField?: { [key: string]: string }; } export type IAuthenticationMethodRegistery = Omit< diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap new file mode 100644 index 000000000000..2bb1bd8053d7 --- /dev/null +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap @@ -0,0 +1,1657 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 1`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: AWS SigV4, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 2`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: No authentication, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 3`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: Username & Password, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 4`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: No authentication, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 5`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: Username & Password, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 6`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: Username & Password, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datasource Management: Create Datasource form with different authType configurations should render normally with all authMethod combinations 7`] = ` + + + } + isOpen={false} + panelPaddingSize="none" + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiSuperSelect" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + +
+
+ +
+ + + +
+
+ + + + Select an option: Username & Password, is selected + + + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index c3c34cbdab1d..4f30ba9da5e4 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -11,7 +11,13 @@ import { OpenSearchDashboardsContextProvider } from '../../../../../../opensearc import { CreateDataSourceForm } from './create_data_source_form'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { AuthType } from '../../../../types'; +import { + AuthType, + noAuthCredentialAuthMethod, + sigV4AuthMethod, + usernamePasswordAuthMethod, +} from '../../../../types'; +import { AuthenticationMethodRegistery } from 'src/plugins/data_source_management/public/auth_registry'; const titleIdentifier = '[data-test-subj="createDataSourceFormTitleField"]'; const descriptionIdentifier = `[data-test-subj="createDataSourceFormDescriptionField"]`; @@ -24,6 +30,14 @@ const testConnectionButtonIdentifier = '[data-test-subj="createDataSourceTestCon describe('Datasource Management: Create Datasource form', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + noAuthCredentialAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + usernamePasswordAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(sigV4AuthMethod); + let component: ReactWrapper, React.Component<{}, {}, any>>; const mockSubmitHandler = jest.fn(); const mockTestConnectionHandler = jest.fn(); @@ -222,3 +236,130 @@ describe('Datasource Management: Create Datasource form', () => { expect(component.find(passwordIdentifier).first().props().isInvalid).toBe(false); }); }); + +describe('Datasource Management: Create Datasource form with different authType configurations', () => { + let component: ReactWrapper, React.Component<{}, {}, any>>; + const mockSubmitHandler = jest.fn(); + const mockTestConnectionHandler = jest.fn(); + const mockCancelHandler = jest.fn(); + + /* Scenario 1: Should render the page normally with all authMethod combinations */ + test('should render normally with all authMethod combinations', () => { + const authMethodCombinationsToBeTested = [ + [sigV4AuthMethod], + [noAuthCredentialAuthMethod], + [usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, sigV4AuthMethod], + [usernamePasswordAuthMethod, sigV4AuthMethod], + [noAuthCredentialAuthMethod, usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, usernamePasswordAuthMethod, sigV4AuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + const authOptionSelector = component.find(authTypeIdentifier).first(); + expect(authOptionSelector).toMatchSnapshot(); + }); + }); + + /* Scenario 2: options selector should be disabled when only one authMethod supported */ + test('options selector should be disabled when less than or equal to one authMethod supported', () => { + const authMethodCombinationsToBeTested = [ + [], + [sigV4AuthMethod], + [noAuthCredentialAuthMethod], + [usernamePasswordAuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + const authOptionSelector = component.find(authTypeIdentifier).last(); + expect(authOptionSelector.prop('disabled')).toBe(true); + }); + }); + + /* Scenario 3: options selector should not be disabled when more than one authMethod supported */ + test('options selector should not be disabled when more than one authMethod supported', () => { + const authMethodCombinationsToBeTested = [ + [sigV4AuthMethod, usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, sigV4AuthMethod], + [noAuthCredentialAuthMethod, usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, sigV4AuthMethod, usernamePasswordAuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + const authOptionSelector = component.find(authTypeIdentifier).last(); + expect(authOptionSelector.prop('disabled')).toBe(false); + }); + }); +}); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index 86052cff8995..e178ea1bcffa 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -18,13 +18,13 @@ import { EuiSuperSelect, EuiSpacer, EuiText, + EuiSuperSelectOption, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { AuthType, - credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, UsernamePasswordTypedContent, @@ -38,7 +38,7 @@ import { isTitleValid, performDataSourceFormValidation, } from '../../../validation'; -import { isValidUrl } from '../../../utils'; +import { getDefaultAuthMethod, isValidUrl } from '../../../utils'; export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; @@ -55,7 +55,7 @@ export interface CreateDataSourceState { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content; + credentials: UsernamePasswordTypedContent | SigV4Content | undefined; }; } @@ -66,19 +66,32 @@ export class CreateDataSourceForm extends React.Component< static contextType = contextType; public readonly context!: DataSourceManagementContextValue; + authOptions: Array> = []; + isNoAuthOptionEnabled: boolean; + constructor(props: CreateDataSourceProps, context: DataSourceManagementContextValue) { super(props, context); + const authenticationMethodRegistery = context.services.authenticationMethodRegistery; + const registeredAuthMethods = authenticationMethodRegistery.getAllAuthenticationMethods(); + const initialSelectedAuthMethod = getDefaultAuthMethod(authenticationMethodRegistery); + + this.isNoAuthOptionEnabled = + authenticationMethodRegistery.getAuthenticationMethod(AuthType.NoAuth) !== undefined; + + this.authOptions = registeredAuthMethods.map((authMethod) => { + return authMethod.credentialSourceOption; + }); + this.state = { formErrorsByField: { ...defaultValidation }, title: '', description: '', endpoint: '', auth: { - type: AuthType.UsernamePasswordType, + type: initialSelectedAuthMethod?.name, credentials: { - username: '', - password: '', + ...initialSelectedAuthMethod?.crendentialFormField, }, }, }; @@ -297,6 +310,9 @@ export class CreateDataSourceForm extends React.Component< service: this.state.auth.credentials.service || SigV4ServiceName.OpenSearch, } as SigV4Content; } + if (this.state.auth.type === AuthType.NoAuth) { + credentials = {}; + } return { title: this.state.title, @@ -583,14 +599,22 @@ export class CreateDataSourceForm extends React.Component< - + {this.isNoAuthOptionEnabled && ( - + )} + {this.isNoAuthOptionEnabled && ( + + + + )} @@ -598,9 +622,10 @@ export class CreateDataSourceForm extends React.Component< this.onChangeAuthType(value)} + disabled={this.authOptions.length <= 1} name="Credential" data-test-subj="createDataSourceFormAuthTypeSelect" /> diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index c57b5b414ed3..4326d6e6832d 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -15,7 +15,12 @@ import { import { OpenSearchDashboardsContextProvider } from '../../../../../../opensearch_dashboards_react/public'; import { EditDataSourceForm } from './edit_data_source_form'; import { act } from 'react-dom/test-utils'; -import { AuthType } from '../../../../types'; +import { + AuthType, + noAuthCredentialAuthMethod, + sigV4AuthMethod, + usernamePasswordAuthMethod, +} from '../../../../types'; const titleFieldIdentifier = 'dataSourceTitle'; const titleFormRowIdentifier = '[data-test-subj="editDataSourceTitleFormRow"]'; @@ -29,6 +34,14 @@ const passwordFieldIdentifier = '[data-test-subj="updateDataSourceFormPasswordFi const updatePasswordBtnIdentifier = '[data-test-subj="editDatasourceUpdatePasswordBtn"]'; describe('Datasource Management: Edit Datasource Form', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + noAuthCredentialAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + usernamePasswordAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(sigV4AuthMethod); + let component: ReactWrapper, React.Component<{}, {}, any>>; const mockFn = jest.fn(); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index 9af843a64071..c3d7daa7db48 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -20,6 +20,7 @@ import { EuiSuperSelect, EuiSpacer, EuiText, + EuiSuperSelectOption, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; @@ -27,7 +28,6 @@ import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/co import { Header } from '../header'; import { AuthType, - credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, sigV4ServiceOptions, @@ -71,10 +71,17 @@ export class EditDataSourceForm extends React.Component> = []; constructor(props: EditDataSourceProps, context: DataSourceManagementContextValue) { super(props, context); + this.authOptions = context.services.authenticationMethodRegistery + .getAllAuthenticationMethods() + .map((authMethod) => { + return authMethod.credentialSourceOption; + }); + this.state = { formErrorsByField: { ...defaultValidation }, title: '', @@ -772,9 +779,10 @@ export class EditDataSourceForm extends React.Component this.onChangeAuthType(value)} + disabled={this.authOptions.length <= 1} name="Credential" data-test-subj="editDataSourceSelectAuthType" /> diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx index 5f6e823e0f86..c1516a507d4a 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx @@ -18,12 +18,25 @@ import { wrapWithIntl } from 'test_utils/enzyme_helpers'; import { RouteComponentProps } from 'react-router-dom'; import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dashboards_react/public'; import { EditDataSource } from './edit_data_source'; +import { + noAuthCredentialAuthMethod, + sigV4AuthMethod, + usernamePasswordAuthMethod, +} from '../../types'; const formIdentifier = 'EditDataSourceForm'; const notFoundIdentifier = '[data-test-subj="dataSourceNotFound"]'; describe('Datasource Management: Edit Datasource Wizard', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + noAuthCredentialAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod( + usernamePasswordAuthMethod + ); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(sigV4AuthMethod); + let component: ReactWrapper, React.Component<{}, {}, any>>; const history = (scopedHistoryMock.create() as unknown) as ScopedHistory; diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index a94d5b2260e6..d19136d62f50 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -9,6 +9,7 @@ import { deleteMultipleDataSources, getDataSourceById, getDataSources, + getDefaultAuthMethod, isValidUrl, testConnection, updateDataSourceById, @@ -23,8 +24,14 @@ import { mockErrorResponseForSavedObjectsCalls, mockResponseForSavedObjectsCalls, } from '../mocks'; -import { AuthType } from '../types'; +import { + AuthType, + noAuthCredentialAuthMethod, + sigV4AuthMethod, + usernamePasswordAuthMethod, +} from '../types'; import { HttpStart } from 'opensearch-dashboards/public'; +import { AuthenticationMethodRegistery } from '../auth_registry'; const { savedObjects } = coreMock.createStart(); @@ -219,4 +226,50 @@ describe('DataSourceManagement: Utils.ts', () => { /* True cases: port number scenario*/ expect(isValidUrl('http://192.168.1.1:1234/')).toBeTruthy(); }); + + describe('Check default auth method', () => { + test('default auth method is Username & Password when Username & Password is enabled', () => { + const authMethodCombinationsToBeTested = [ + [usernamePasswordAuthMethod], + [sigV4AuthMethod, usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, usernamePasswordAuthMethod], + [noAuthCredentialAuthMethod, sigV4AuthMethod, usernamePasswordAuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authOptions) => { + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authOptions.forEach((authMethod) => { + authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + expect(getDefaultAuthMethod(authenticationMethodRegistery)?.name).toBe( + AuthType.UsernamePasswordType + ); + }); + }); + + test('default auth method is first one in AuthList when Username & Password is not enabled', () => { + const authMethodCombinationsToBeTested = [ + [sigV4AuthMethod], + [noAuthCredentialAuthMethod], + [sigV4AuthMethod, noAuthCredentialAuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authOptions) => { + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authOptions.forEach((authMethod) => { + authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + expect(getDefaultAuthMethod(authenticationMethodRegistery)?.name).toBe(authOptions[0].name); + }); + }); + + test('default auth type is NoAuth when no auth options registered in authenticationMethodRegistery, this should not happen in real customer scenario for MD', () => { + const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + expect(getDefaultAuthMethod(authenticationMethodRegistery)?.name).toBe(AuthType.NoAuth); + }); + }); }); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 5f2cfb2337ad..726fcc527f30 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -4,7 +4,13 @@ */ import { HttpStart, SavedObjectsClientContract } from 'src/core/public'; -import { DataSourceAttributes, DataSourceTableItem } from '../types'; +import { + DataSourceAttributes, + DataSourceTableItem, + defaultAuthType, + noAuthCredentialAuthMethod, +} from '../types'; +import { AuthenticationMethodRegistery } from '../auth_registry'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -108,3 +114,19 @@ export const isValidUrl = (endpoint: string) => { return false; } }; + +export const getDefaultAuthMethod = ( + authenticationMethodRegistery: AuthenticationMethodRegistery +) => { + const registeredAuthMethods = authenticationMethodRegistery.getAllAuthenticationMethods(); + + const defaultAuthMethod = + registeredAuthMethods.length > 0 + ? authenticationMethodRegistery.getAuthenticationMethod(registeredAuthMethods[0].name) + : noAuthCredentialAuthMethod; + + const initialSelectedAuthMethod = + authenticationMethodRegistery.getAuthenticationMethod(defaultAuthType) ?? defaultAuthMethod; + + return initialSelectedAuthMethod; +}; diff --git a/src/plugins/data_source_management/public/management_app/mount_management_section.tsx b/src/plugins/data_source_management/public/management_app/mount_management_section.tsx index 9fe1f2406382..6b421a32d2b2 100644 --- a/src/plugins/data_source_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_source_management/public/management_app/mount_management_section.tsx @@ -17,6 +17,7 @@ import { CreateDataSourceWizardWithRouter } from '../components/create_data_sour import { DataSourceTableWithRouter } from '../components/data_source_table'; import { DataSourceManagementContext } from '../types'; import { EditDataSourceWithRouter } from '../components/edit_data_source'; +import { AuthenticationMethodRegistery } from '../auth_registry'; export interface DataSourceManagementStartDependencies { data: DataPublicPluginStart; @@ -24,7 +25,8 @@ export interface DataSourceManagementStartDependencies { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams + params: ManagementAppMountParams, + authMethodsRegistry: AuthenticationMethodRegistery ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, @@ -40,6 +42,7 @@ export async function mountManagementSection( http, docLinks, setBreadcrumbs: params.setBreadcrumbs, + authenticationMethodRegistery: authMethodsRegistry, }; ReactDOM.render( diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 7b170c4a7c79..71e0310cb870 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -15,7 +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'; +import { AuthenticationMethod, AuthenticationMethodRegistery } from './auth_registry'; /* Mock Types */ @@ -30,6 +30,8 @@ export const docLinks = { }, }; +export const authenticationMethodRegistery = new AuthenticationMethodRegistery(); + const createDataSourceManagementContext = () => { const { chrome, @@ -51,6 +53,7 @@ const createDataSourceManagementContext = () => { http, docLinks, setBreadcrumbs: () => {}, + authenticationMethodRegistery, }; }; @@ -220,6 +223,13 @@ export const testDataSourceManagementPlugin = ( const setup = plugin.setup(coreSetup, { management: managementPluginMock.createSetupContract(), indexPatternManagement: indexPatternManagementPluginMock.createSetupContract(), + dataSource: { + dataSourceEnabled: true, + hideLocalCluster: true, + noAuthenticationTypeEnabled: true, + usernamePasswordAuthEnabled: true, + awsSigV4AuthEnabled: true, + }, }); const doStart = () => { const start = plugin.start(coreStart); diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index 0c7123e47a94..131e38a9765c 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { DataSourcePluginSetup } from 'src/plugins/data_source/public'; import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { PLUGIN_NAME } from '../common'; @@ -15,10 +16,12 @@ import { IAuthenticationMethodRegistery, AuthenticationMethodRegistery, } from './auth_registry'; +import { noAuthCredentialAuthMethod, sigV4AuthMethod, usernamePasswordAuthMethod } from './types'; export interface DataSourceManagementSetupDependencies { management: ManagementSetup; indexPatternManagement: IndexPatternManagementSetup; + dataSource: DataSourcePluginSetup; } export interface DataSourceManagementPluginSetup { @@ -43,7 +46,7 @@ export class DataSourceManagementPlugin public setup( core: CoreSetup, - { management, indexPatternManagement }: DataSourceManagementSetupDependencies + { management, indexPatternManagement, dataSource }: DataSourceManagementSetupDependencies ) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; @@ -65,7 +68,7 @@ export class DataSourceManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params); + return mountManagementSection(core.getStartServices, params, this.authMethodsRegistry); }, }); @@ -78,6 +81,16 @@ export class DataSourceManagementPlugin this.authMethodsRegistry.registerAuthenticationMethod(authMethod); }; + if (dataSource.noAuthenticationTypeEnabled) { + registerAuthenticationMethod(noAuthCredentialAuthMethod); + } + if (dataSource.usernamePasswordAuthEnabled) { + registerAuthenticationMethod(usernamePasswordAuthMethod); + } + if (dataSource.awsSigV4AuthEnabled) { + registerAuthenticationMethod(sigV4AuthMethod); + } + return { registerAuthenticationMethod }; } diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index d461daba82cc..b0dd6caf1a11 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -18,6 +18,7 @@ import { SavedObjectAttributes } from 'src/core/types'; import { i18n } from '@osd/i18n'; import { SigV4ServiceName } from '../../data_source/common/data_sources'; import { OpenSearchDashboardsReactContextValue } from '../../opensearch_dashboards_react/public'; +import { AuthenticationMethodRegistery } from './auth_registry'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataSourceManagementPluginStart {} @@ -32,6 +33,7 @@ export interface DataSourceManagementContext { http: HttpSetup; docLinks: DocLinksStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + authenticationMethodRegistery: AuthenticationMethodRegistery; } export interface DataSourceTableItem { @@ -58,26 +60,47 @@ export enum AuthType { SigV4 = 'sigv4', } -export const credentialSourceOptions = [ - { - value: AuthType.NoAuth, - inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.NoAuthentication', { - defaultMessage: 'No authentication', - }), - }, - { - value: AuthType.UsernamePasswordType, - inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.UsernamePassword', { - defaultMessage: 'Username & Password', - }), - }, - { - value: AuthType.SigV4, - inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.AwsSigV4', { - defaultMessage: 'AWS SigV4', - }), - }, -]; +export const defaultAuthType = AuthType.UsernamePasswordType; + +export const noAuthCredentialOption = { + value: AuthType.NoAuth, + inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.NoAuthentication', { + defaultMessage: 'No authentication', + }), +}; + +export const noAuthCredentialField = {}; + +export const noAuthCredentialAuthMethod = { + name: AuthType.NoAuth, + credentialSourceOption: noAuthCredentialOption, + crendentialFormField: noAuthCredentialField, +}; + +export const usernamePasswordCredentialOption = { + value: AuthType.UsernamePasswordType, + inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.UsernamePassword', { + defaultMessage: 'Username & Password', + }), +}; + +export const usernamePasswordCredentialField = { + username: '', + password: '', +}; + +export const usernamePasswordAuthMethod = { + name: AuthType.UsernamePasswordType, + credentialSourceOption: usernamePasswordCredentialOption, + crendentialFormField: usernamePasswordCredentialField, +}; + +export const sigV4CredentialOption = { + value: AuthType.SigV4, + inputDisplay: i18n.translate('dataSourceManagement.credentialSourceOptions.AwsSigV4', { + defaultMessage: 'AWS SigV4', + }), +}; export const sigV4ServiceOptions = [ { @@ -94,6 +117,25 @@ export const sigV4ServiceOptions = [ }, ]; +export const sigV4CredentialField = { + region: '', + accessKey: '', + secretKey: '', + service: '', +}; + +export const sigV4AuthMethod = { + name: AuthType.SigV4, + credentialSourceOption: sigV4CredentialOption, + crendentialFormField: sigV4CredentialField, +}; + +export const credentialSourceOptions = [ + noAuthCredentialOption, + usernamePasswordCredentialOption, + sigV4CredentialOption, +]; + export interface DataSourceAttributes extends SavedObjectAttributes { title: string; description?: string;