From 4ba9fcf48935e866a1c32a14207e2e8b860c9181 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 20 Mar 2024 10:52:25 -0400 Subject: [PATCH] Register Datasource Picker in the top nav menu for Get Started Tab (#1818) Signed-off-by: Darshit Chanpura Signed-off-by: Derek Ho Co-authored-by: Darshit Chanpura --- ...ess-test-multidatasources-disabled-e2e.yml | 49 +++++++ ...ress-test-multidatasources-enabled-e2e.yml | 120 ++++++++++++++++++ .github/workflows/integration-test.yml | 56 ++++++++ opensearch_dashboards.json | 4 +- public/apps/configuration/app-router.tsx | 2 +- .../apps/configuration/configuration-app.tsx | 14 +- .../apps/configuration/panels/get-started.tsx | 40 +++++- .../__snapshots__/get-started.test.tsx.snap | 36 ++++++ .../panels/test/get-started.test.tsx | 16 ++- .../configuration/test/top-nav-menu.test.tsx | 78 ++++++++++++ public/apps/configuration/top-nav-menu.tsx | 47 +++++++ .../apps/configuration/utils/request-utils.ts | 4 +- public/apps/types.ts | 4 +- public/plugin.ts | 8 +- public/types.ts | 9 ++ server/plugin.ts | 16 ++- server/routes/index.ts | 65 ++++++++-- .../multi_datasources_disabled.spec.js | 27 ++++ .../multi_datasources_enabled.spec.js | 69 ++++++++++ test/cypress/support/commands.js | 14 ++ .../security_entity_api.test.ts | 107 +++++++++++++++- 21 files changed, 748 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/cypress-test-multidatasources-disabled-e2e.yml create mode 100644 .github/workflows/cypress-test-multidatasources-enabled-e2e.yml create mode 100644 public/apps/configuration/test/top-nav-menu.test.tsx create mode 100644 public/apps/configuration/top-nav-menu.tsx create mode 100644 test/cypress/e2e/multi-datasources/multi_datasources_disabled.spec.js create mode 100644 test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js diff --git a/.github/workflows/cypress-test-multidatasources-disabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-disabled-e2e.yml new file mode 100644 index 000000000..e41de5fab --- /dev/null +++ b/.github/workflows/cypress-test-multidatasources-disabled-e2e.yml @@ -0,0 +1,49 @@ +name: E2E multi datasources disabled workflow + +on: [ push, pull_request ] + +env: + OPENSEARCH_VERSION: '3.0.0' + CI: 1 + # avoid warnings like "tput: No value for $TERM and no -T specified" + TERM: xterm + PLUGIN_NAME: opensearch-security + OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! + +jobs: + tests: + name: Run Cypress multidatasources tests + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout Branch + uses: actions/checkout@v3 + + # Configure the Dashboard for multi datasources disabled (default) + - name: Create OpenSearch Dashboards Config + if: ${{ runner.os == 'Linux' }} + run: | + cat << 'EOT' > opensearch_dashboards_multidatasources.yml + server.host: "0.0.0.0" + opensearch.hosts: ["https://localhost:9200"] + opensearch.ssl.verificationMode: none + opensearch.username: "kibanaserver" + opensearch.password: "kibanaserver" + opensearch.requestHeadersWhitelist: [ authorization,securitytenant ] + opensearch_security.multitenancy.enabled: false + opensearch_security.multitenancy.tenants.preferred: ["Private", "Global"] + opensearch_security.readonly_mode.roles: ["kibana_read_only"] + opensearch_security.cookie.secure: false + data_source.enabled: false + home.disableWelcomeScreen: true + EOT + + - name: Run Cypress Tests + uses: ./.github/actions/run-cypress-tests + with: + dashboards_config_file: opensearch_dashboards_multidatasources.yml + yarn_command: 'yarn cypress:run --browser chrome --headless --env LOGIN_AS_ADMIN=true --spec "test/cypress/e2e/multi-datasources/multi_datasources_disabled.spec.js"' diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml new file mode 100644 index 000000000..5a97342bb --- /dev/null +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -0,0 +1,120 @@ +name: E2E multi datasources enabled workflow + +on: [ push, pull_request ] + +env: + OPENSEARCH_VERSION: '3.0.0' + CI: 1 + # avoid warnings like "tput: No value for $TERM and no -T specified" + TERM: xterm + PLUGIN_NAME: opensearch-security + OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! + +jobs: + tests: + name: Run Cypress multidatasources tests + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout Branch + uses: actions/checkout@v3 + + - name: Set env + run: | + opensearch_version=$(node -p "require('./package.json').opensearchDashboards.version") + plugin_version=$(node -p "require('./package.json').version") + echo "OPENSEARCH_VERSION=$opensearch_version" >> $GITHUB_ENV + echo "PLUGIN_VERSION=$plugin_version" >> $GITHUB_ENV + shell: bash + + - name: Create remote OpenSearch Config + if: ${{ runner.os == 'Linux' }} + run: | + cat << 'EOT' > remote_opensearch.yml + http.port: 9202 + plugins.security.ssl.transport.pemcert_filepath: esnode.pem + plugins.security.ssl.transport.pemkey_filepath: esnode-key.pem + plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem + plugins.security.ssl.transport.enforce_hostname_verification: false + plugins.security.ssl.http.pemcert_filepath: esnode.pem + plugins.security.ssl.http.pemkey_filepath: esnode-key.pem + plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem + plugins.security.allow_unsafe_democertificates: true + plugins.security.allow_default_init_securityindex: true + plugins.security.authcz.admin_dn: + - 'CN=A,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + plugins.security.nodes_dn: + - 'CN=node1.dns.a-record,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + - 'CN=node2.dns.a-record,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + plugins.security.audit.type: internal_opensearch + plugins.security.enable_snapshot_restore_privilege: true + plugins.security.check_snapshot_restore_write_privileges: true + # TODO: change this back to true/just append to the created opensearch.yml the new port + # after the self-signed certs issue is fixed + plugins.security.ssl.http.enabled: false + plugins.security.restapi.roles_enabled: [all_access, security_rest_api_access] + plugins.security.system_indices.enabled: true + plugins.security.system_indices.indices: [.plugins-ml-config, .plugins-ml-connector, + .plugins-ml-model-group, .plugins-ml-model, .plugins-ml-task, .plugins-ml-conversation-meta, + .plugins-ml-conversation-interactions, .plugins-ml-memory-meta, .plugins-ml-memory-message, + .opendistro-alerting-config, .opendistro-alerting-alert*, .opendistro-anomaly-results*, + .opendistro-anomaly-detector*, .opendistro-anomaly-checkpoints, .opendistro-anomaly-detection-state, + .opendistro-reports-*, .opensearch-notifications-*, .opensearch-notebooks, .opensearch-observability, + .ql-datasources, .opendistro-asynchronous-search-response*, .replication-metadata-store, + .opensearch-knn-models, .geospatial-ip2geo-data*, .plugins-flow-framework-config, + .plugins-flow-framework-templates, .plugins-flow-framework-state] + node.max_local_storage_nodes: 3 + EOT + + - name: Download security plugin and create setup scripts + uses: ./.github/actions/download-plugin + with: + opensearch-version: ${{ env.OPENSEARCH_VERSION }} + plugin-name: ${{ env.PLUGIN_NAME }} + plugin-version: ${{ env.PLUGIN_VERSION }} + + - name: Run Opensearch with A Single Plugin + uses: derek-ho/start-opensearch@9202 + with: + opensearch-version: ${{ env.OPENSEARCH_VERSION }} + plugins: "file:$(pwd)/opensearch-security.zip" + security-enabled: true + admin-password: ${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} + security_config_file: ${{ inputs.security_config_file }} + opensearch_yml_file: remote_opensearch.yml + opensearch_port: 9202 + + - name: Check OpenSearch is running + # Verify that the server is operational + run: | + curl http://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! + shell: bash + + # Configure the Dashboard for multi datasources + - name: Create OpenSearch Dashboards Config + if: ${{ runner.os == 'Linux' }} + run: | + cat << 'EOT' > opensearch_dashboards_multidatasources.yml + server.host: "localhost" + opensearch.hosts: ["https://localhost:9200"] + opensearch.ssl.verificationMode: none + opensearch.username: "kibanaserver" + opensearch.password: "kibanaserver" + opensearch.requestHeadersWhitelist: [ authorization,securitytenant ] + opensearch_security.multitenancy.enabled: true + opensearch_security.multitenancy.tenants.preferred: ["Private", "Global"] + opensearch_security.readonly_mode.roles: ["kibana_read_only"] + opensearch_security.cookie.secure: false + data_source.enabled: true + home.disableWelcomeScreen: true + EOT + + - name: Run Cypress Tests + uses: ./.github/actions/run-cypress-tests + with: + dashboards_config_file: opensearch_dashboards_multidatasources.yml + yarn_command: 'yarn cypress:run --browser chrome --headed --env LOGIN_AS_ADMIN=true --spec "test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js"' diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 40dcdfecc..8948f14b0 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -59,6 +59,62 @@ jobs: plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + - name: Create remote OpenSearch Config + run: | + cat << 'EOT' > remote_opensearch.yml + http.port: 9202 + plugins.security.ssl.transport.pemcert_filepath: esnode.pem + plugins.security.ssl.transport.pemkey_filepath: esnode-key.pem + plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem + plugins.security.ssl.transport.enforce_hostname_verification: false + plugins.security.ssl.http.pemcert_filepath: esnode.pem + plugins.security.ssl.http.pemkey_filepath: esnode-key.pem + plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem + plugins.security.allow_unsafe_democertificates: true + plugins.security.allow_default_init_securityindex: true + plugins.security.authcz.admin_dn: + - 'CN=A,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + plugins.security.nodes_dn: + - 'CN=node1.dns.a-record,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + - 'CN=node2.dns.a-record,OU=UNIT,O=ORG,L=TORONTO,ST=ONTARIO,C=CA' + plugins.security.audit.type: internal_opensearch + plugins.security.enable_snapshot_restore_privilege: true + plugins.security.check_snapshot_restore_write_privileges: true + # TODO: change this back to true/just append to the created opensearch.yml the new port + # after the self-signed certs issue is fixed + plugins.security.ssl.http.enabled: false + plugins.security.restapi.roles_enabled: [all_access, security_rest_api_access] + plugins.security.system_indices.enabled: true + plugins.security.system_indices.indices: [.plugins-ml-config, .plugins-ml-connector, + .plugins-ml-model-group, .plugins-ml-model, .plugins-ml-task, .plugins-ml-conversation-meta, + .plugins-ml-conversation-interactions, .plugins-ml-memory-meta, .plugins-ml-memory-message, + .opendistro-alerting-config, .opendistro-alerting-alert*, .opendistro-anomaly-results*, + .opendistro-anomaly-detector*, .opendistro-anomaly-checkpoints, .opendistro-anomaly-detection-state, + .opendistro-reports-*, .opensearch-notifications-*, .opensearch-notebooks, .opensearch-observability, + .ql-datasources, .opendistro-asynchronous-search-response*, .replication-metadata-store, + .opensearch-knn-models, .geospatial-ip2geo-data*, .plugins-flow-framework-config, + .plugins-flow-framework-templates, .plugins-flow-framework-state] + node.max_local_storage_nodes: 3 + EOT + shell: bash + + - name: Run Opensearch with A Single Plugin + uses: derek-ho/start-opensearch@9202 + with: + opensearch-version: ${{ env.OPENSEARCH_VERSION }} + plugins: "file:$(pwd)/opensearch-security.zip" + security-enabled: true + admin-password: ${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} + security_config_file: ${{ inputs.security_config_file }} + opensearch_yml_file: remote_opensearch.yml + opensearch_port: 9202 + + - name: Check OpenSearch is running + # Verify that the server is operational + run: | + curl http://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! + shell: bash + - name: Run Opensearch with security uses: derek-ho/start-opensearch@v2 with: diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 994096906..3a2642593 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -10,7 +10,9 @@ "savedObjectsManagement" ], "optionalPlugins": [ - "managementOverview" + "managementOverview", + "dataSource", + "dataSourceManagement" ], "server": true, "ui": true diff --git a/public/apps/configuration/app-router.tsx b/public/apps/configuration/app-router.tsx index 2e4ce7ca3..e063463f0 100644 --- a/public/apps/configuration/app-router.tsx +++ b/public/apps/configuration/app-router.tsx @@ -14,7 +14,7 @@ */ import { EuiBreadcrumb, EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; -import { flow, map, mapValues, partial } from 'lodash'; +import { flow, partial } from 'lodash'; import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; import { AppDependencies } from '../types'; diff --git a/public/apps/configuration/configuration-app.tsx b/public/apps/configuration/configuration-app.tsx index a2294315d..1f341f781 100644 --- a/public/apps/configuration/configuration-app.tsx +++ b/public/apps/configuration/configuration-app.tsx @@ -21,14 +21,22 @@ import { I18nProvider } from '@osd/i18n/react'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { SecurityPluginStartDependencies, ClientConfigType } from '../../types'; import { AppRouter } from './app-router'; +import { DataSourceManagementPluginSetup } from '../../../../../src/plugins/data_source_management/public'; export function renderApp( coreStart: CoreStart, - navigation: SecurityPluginStartDependencies, + depsStart: SecurityPluginStartDependencies, params: AppMountParameters, - config: ClientConfigType + config: ClientConfigType, + dataSourceManagement?: DataSourceManagementPluginSetup ) { - const deps = { coreStart, navigation, params, config }; + const deps = { + coreStart, + depsStart, + params, + config, + dataSourceManagement, + }; ReactDOM.render( diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index 806d8d2f4..a8a082e22 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -26,7 +26,7 @@ import { EuiTitle, EuiGlobalToastList, } from '@elastic/eui'; -import React from 'react'; +import React, { useState } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { AppDependencies } from '../../types'; import { buildHashUrl } from '../utils/url-builder'; @@ -36,6 +36,8 @@ import { API_ENDPOINT_CACHE, DocLinks } from '../constants'; import { ExternalLink, ExternalLinkButton } from '../utils/display-utils'; import { httpDelete } from '../utils/request-utils'; import { createSuccessToast, createUnknownErrorToast, useToastState } from '../utils/toast-utils'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; +import { Cluster } from '../../../types'; const addBackendStep = { title: 'Add backends', @@ -158,7 +160,17 @@ const setOfSteps = [ }, ]; +export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: Cluster) { + if (dataSourceEnabled) { + return `for ${cluster.label || 'Local cluster'}`; + } + return ''; +} + export function GetStarted(props: AppDependencies) { + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; + const [dataSource, setDataSource] = useState({ id: '', label: '' }); + let steps; if (props.config.ui.backend_configurable) { steps = [addBackendStep, ...setOfSteps]; @@ -170,6 +182,11 @@ export function GetStarted(props: AppDependencies) { return ( <>
+

Get started

@@ -236,16 +253,29 @@ export function GetStarted(props: AppDependencies) { data-test-subj="purge-cache" onClick={async () => { try { - await httpDelete(props.coreStart.http, API_ENDPOINT_CACHE); + await httpDelete(props.coreStart.http, API_ENDPOINT_CACHE, { + dataSourceId: dataSource.id, + }); addToast( createSuccessToast( 'cache-flush-success', - 'Cache purge successful', - 'Cache purge successful' + `Cache purge successful ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}`, + `Cache purge successful ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}` ) ); } catch (err) { - addToast(createUnknownErrorToast('cache-flush-failed', 'purge cache')); + addToast( + createUnknownErrorToast( + 'cache-flush-failed', + `purge cache ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` + ) + ); } }} > diff --git a/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap index 82c3ec0b8..81d2bfb1d 100644 --- a/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap @@ -5,6 +5,24 @@ exports[`Get started (landing page) renders when backend configuration is disabl
+ + { const component = shallow( ); expect(component).toMatchSnapshot(); @@ -64,9 +64,9 @@ describe('Get started (landing page)', () => { const component = shallow( ); expect(component).toMatchSnapshot(); @@ -78,9 +78,9 @@ describe('Get started (landing page)', () => { wrapper = shallow( ); jest.clearAllMocks(); @@ -140,9 +140,9 @@ describe('Get started (landing page)', () => { wrapper = shallow( ); jest.clearAllMocks(); @@ -170,5 +170,11 @@ describe('Get started (landing page)', () => { await button.props().onClick(); // Simulate button click expect(ToastUtils.createSuccessToast).toHaveBeenCalledTimes(1); }); + + it('Tests the GetClusterDescription helper function', () => { + expect(getClusterInfoIfEnabled(false, { id: 'blah', label: 'blah' })).toBe(''); + expect(getClusterInfoIfEnabled(true, { id: '', label: '' })).toBe('for Local cluster'); + expect(getClusterInfoIfEnabled(true, { id: 'test', label: 'test' })).toBe('for test'); + }); }); }); diff --git a/public/apps/configuration/test/top-nav-menu.test.tsx b/public/apps/configuration/test/top-nav-menu.test.tsx new file mode 100644 index 000000000..93fef3d80 --- /dev/null +++ b/public/apps/configuration/test/top-nav-menu.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import { render } from 'enzyme'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; + +describe('SecurityPluginTopNavMenu', () => { + const coreStartMock = { + savedObjects: { + client: jest.fn(), + }, + notifications: jest.fn(), + }; + + const dataSourceMenuMock = jest.fn(() =>
Mock DataSourceMenu
); + + const dataSourceManagementMock = { + ui: { + DataSourceMenu: dataSourceMenuMock, + }, + }; + + it('renders DataSourceMenu when dataSource is enabled', () => { + const securityPluginStartDepsMock = { + dataSource: { + dataSourceEnabled: true, + }, + }; + + const wrapper = render( + + ); + + expect(dataSourceMenuMock).toBeCalled(); + expect(wrapper.html()).not.toBe(''); + }); + + it('renders null when dataSource is disabled', () => { + const securityPluginStartDepsMock = { + dataSource: { + dataSourceEnabled: false, + }, + }; + + const wrapper = render( + {}} + params={{}} + /> + ); + + expect(dataSourceMenuMock).not.toBeCalled(); + expect(wrapper.html()).toBe(''); + }); +}); diff --git a/public/apps/configuration/top-nav-menu.tsx b/public/apps/configuration/top-nav-menu.tsx new file mode 100644 index 000000000..b36d46e6e --- /dev/null +++ b/public/apps/configuration/top-nav-menu.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public/types'; +import { ClientConfigType } from '../../types'; +import { PLUGIN_NAME } from '../../../common'; +import { AppDependencies } from '../types'; +import { Cluster } from '../../types'; + +export interface TopNavMenuProps extends AppDependencies { + dataSourcePickerReadOnly: boolean; + setDatasourceId: React.Dispatch>; +} + +export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { + const { coreStart, depsStart, params, dataSourceManagement, setDatasourceId } = props; + const { setHeaderActionMenu } = params; + const DataSourceMenu = dataSourceManagement?.ui.DataSourceMenu; + const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; + + return dataSourceEnabled ? ( + setDatasourceId(datasource)} + hideLocalCluster={false} + fullWidth={false} + /> + ) : null; +}; diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index f348f49ad..20502309f 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -34,8 +34,8 @@ export async function httpPut(http: HttpStart, url: string, body?: object): P return await request(http.put, url, body); } -export async function httpDelete(http: HttpStart, url: string): Promise { - return await request(http.delete, url); +export async function httpDelete(http: HttpStart, url: string, body?: object): Promise { + return await request(http.delete, url, body); } /** diff --git a/public/apps/types.ts b/public/apps/types.ts index 43d723563..276f1ce07 100644 --- a/public/apps/types.ts +++ b/public/apps/types.ts @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ +import { DataSourceManagementPluginSetup } from '../../../../src/plugins/data_source_management/public'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; import { SecurityPluginStartDependencies, ClientConfigType, DashboardsInfo } from '../types'; export interface AppDependencies { coreStart: CoreStart; - navigation: SecurityPluginStartDependencies; + depsStart: SecurityPluginStartDependencies; params: AppMountParameters; config: ClientConfigType; dashboardsInfo: DashboardsInfo; + dataSourceManagement: DataSourceManagementPluginSetup; } export interface BreadcrumbsPageDependencies extends AppDependencies { diff --git a/public/plugin.ts b/public/plugin.ts index 7ac039fb1..ca9798a94 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -112,7 +112,13 @@ export class SecurityPlugin excludeFromDisabledTransportCategories(config.disabledTransportCategories.exclude); excludeFromDisabledRestCategories(config.disabledRestCategories.exclude); - return renderApp(coreStart, depsStart as SecurityPluginStartDependencies, params, config); + return renderApp( + coreStart, + depsStart as SecurityPluginStartDependencies, + params, + config, + deps.dataSourceManagement + ); }, category: DEFAULT_APP_CATEGORIES.management, }); diff --git a/public/types.ts b/public/types.ts index 4acfc442f..96e587354 100644 --- a/public/types.ts +++ b/public/types.ts @@ -19,6 +19,8 @@ import { SavedObjectsManagementPluginStart, } from '../../../src/plugins/saved_objects_management/public'; import { ManagementOverViewPluginSetup } from '../../../src/plugins/management_overview/public'; +import { DataSourcePluginStart } from '../../../src/plugins/data_source/public/types'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SecurityPluginSetup {} @@ -28,11 +30,18 @@ export interface SecurityPluginStart {} export interface SecurityPluginSetupDependencies { savedObjectsManagement: SavedObjectsManagementPluginSetup; managementOverview?: ManagementOverViewPluginSetup; + dataSourceManagement?: DataSourceManagementPluginSetup; +} + +export interface Cluster { + id: string; + label: string; } export interface SecurityPluginStartDependencies { navigation: NavigationPublicPluginStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + dataSource?: DataSourcePluginStart; } export interface AuthInfo { diff --git a/server/plugin.ts b/server/plugin.ts index 5f5f50913..e7877ad62 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -46,12 +46,19 @@ import { createMigrationOpenSearchClient } from '../../../src/core/server/saved_ import { SecuritySavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper'; import { addTenantParameterToResolvedShortLink } from './multitenancy/tenant_resolver'; import { ReadonlyService } from './readonly/readonly_service'; +import { DataSourceManagementPlugin } from '../../../src/plugins/data_source_management/public/plugin'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/server/types'; export interface SecurityPluginRequestContext { logger: Logger; esClient: ILegacyClusterClient; } +export interface SecurityPluginSetupDependencies { + dataSourceManagement: ReturnType; + dataSource: DataSourcePluginSetup; +} + declare module 'opensearch-dashboards/server' { interface RequestHandlerContext { security_plugin: SecurityPluginRequestContext; @@ -83,8 +90,9 @@ export class SecurityPlugin implements Plugin(); const config: SecurityPluginConfigType = await config$.pipe(first()).toPromise(); @@ -97,6 +105,10 @@ export class SecurityPlugin implements Plugin { - const client = context.security_plugin.esClient.asScoped(request); - let esResponse; - try { - esResponse = await client.callAsCurrentUser('opensearch_security.clearCache'); - return response.ok({ - body: { - message: esResponse.message, - }, - }); - } catch (error) { - return errorResponse(response, error); - } + return wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + response, + 'opensearch_security.clearCache' + ); } ); @@ -883,6 +884,42 @@ export function defineRoutes(router: IRouter) { ); } +const wrapRouteWithDataSource = async ( + dataSourceEnabled: boolean, + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + endpoint: string +) => { + if (!dataSourceEnabled || !request.body?.dataSourceId) { + const client = context.security_plugin.esClient.asScoped(request); + let esResponse; + try { + esResponse = await client.callAsCurrentUser(endpoint); + return response.ok({ + body: { + message: esResponse.message, + }, + }); + } catch (error) { + return errorResponse(response, error); + } + } else { + const client = context.dataSource.opensearch.legacy.getClient(request.body?.dataSourceId); + let esResponse; + try { + esResponse = await client.callAPI(endpoint, {}); + return response.ok({ + body: { + message: esResponse.message, + }, + }); + } catch (error) { + return errorResponse(response, error); + } + } +}; + function parseEsErrorResponse(error: any) { if (error.response) { try { diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_disabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_disabled.spec.js new file mode 100644 index 000000000..8cc4495b9 --- /dev/null +++ b/test/cypress/e2e/multi-datasources/multi_datasources_disabled.spec.js @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +describe('Multi-datasources enabled', () => { + it('Sanity checks the cluster selector is not visible when multi datasources is disabled', () => { + localStorage.setItem('opendistro::security::tenant::saved', '""'); + localStorage.setItem('home:newThemeModal:show', 'false'); + + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/getstarted', { + failOnStatusCode: false, + }); + + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should('not.exist'); + }); +}); diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js new file mode 100644 index 000000000..35de36c36 --- /dev/null +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const createDataSource = () => { + cy.request({ + method: 'POST', + url: `${Cypress.config('baseUrl')}/api/saved_objects/data-source`, + headers: { + 'osd-xsrf': true, + }, + body: { + attributes: { + title: `9202`, + endpoint: `http://localhost:9202`, + auth: { + type: 'username_password', + credentials: { + username: 'admin', + password: 'myStrongPassword123!', + }, + }, + }, + }, + }); +}; + +const deleteAllDataSources = () => { + cy.visit('http://localhost:5601/app/management/opensearch-dashboards/dataSources'); + cy.get('[data-test-subj="checkboxSelectAll"]').click(); + cy.get('[data-test-subj="deleteDataSourceConnections"]').click(); + cy.get('[data-test-subj="confirmModalConfirmButton"]').click(); +}; + +describe('Multi-datasources enabled', () => { + before(() => { + localStorage.setItem('opendistro::security::tenant::saved', '""'); + localStorage.setItem('home:newThemeModal:show', 'false'); + createDataSource(); + }); + + after(() => { + deleteAllDataSources(); + cy.clearLocalStorage(); + }); + + it('Checks Get Started Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/getstarted'); + // Local cluster purge cache + cy.get('[data-test-subj="purge-cache"]').click(); + cy.get('.euiToastHeader__title').should('contain', 'successful for Local cluster'); + // Remote cluster purge cache + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('[data-test-subj="purge-cache"]').click(); + cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); + }); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 29c565062..cdab9ed22 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -91,6 +91,20 @@ Cypress.Commands.add('loginWithSamlMultiauth', () => { cy.get('button[id=btn-sign-in]').should('be.visible').click(); }); +if (Cypress.env('LOGIN_AS_ADMIN')) { + // Define custom cy.visit() only if LOGIN_AS_ADMIN is true + Cypress.Commands.overwrite('visit', (orig, url, options = {}) => { + if (Cypress.env('LOGIN_AS_ADMIN')) { + options.auth = ADMIN_AUTH; + options.failOnStatusCode = false; + options.qs = { + security_tenant: 'private', + }; + } + orig(url, options); + }); +} + Cypress.Commands.add('shortenUrl', (data, tenant) => { cy.request({ url: `http://localhost:5601${DASHBOARDS_API.SHORTEN_URL}`, diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index f28817798..e179cd136 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -351,15 +351,31 @@ describe('start OpenSearch Dashboards server', () => { it('delete cache', async () => { const deleteCacheResponse = await osdTestServer.request .delete(root, '/api/v1/configuration/cache') - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: '' }); expect(deleteCacheResponse.status).toEqual(200); + // Multi datasources not enabled so dataSourceId is not read + const deleteCacheResponseMultiDataSource = await osdTestServer.request + .delete(root, '/api/v1/configuration/cache') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: 'Derek Datasource' }); + expect(deleteCacheResponseMultiDataSource.status).toEqual(200); + const adminAuthCookie = await getAuthCookie(root, ADMIN_USER, ADMIN_PASSWORD); const deleteCacheWithCookieResponse = await osdTestServer.request .delete(root, '/api/v1/configuration/cache') .unset(AUTHORIZATION_HEADER_NAME) - .set('Cookie', adminAuthCookie); + .set('Cookie', adminAuthCookie) + .send({ dataSourceId: '' }); expect(deleteCacheWithCookieResponse.status).toEqual(200); + + // Multi datasources not enabled so dataSourceId is not read + const deleteCacheResponseMultiDataSourceCookie = await osdTestServer.request + .delete(root, '/api/v1/configuration/cache') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: 'Derek Datasource' }); + expect(deleteCacheResponseMultiDataSourceCookie.status).toEqual(200); }); it('restapiinfo', async () => { @@ -406,3 +422,90 @@ describe('start OpenSearch Dashboards server', () => { expect(response.status).toEqual(200); }); }); + +describe('start OpenSearch Dashboards server multi datasources enabled', () => { + let root: Root; + + beforeAll(async () => { + root = osdTestServer.createRootWithSettings( + { + plugins: { + scanDirs: [resolve(__dirname, '../..')], + }, + data_source: { enabled: true }, + opensearch: { + hosts: ['https://localhost:9200'], + ignoreVersionMismatch: true, + ssl: { verificationMode: 'none' }, + username: OPENSEARCH_DASHBOARDS_SERVER_USER, + password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + }, + opensearch_security: { + multitenancy: { enabled: true, tenants: { preferred: ['Private', 'Global'] } }, + }, + }, + { + // to make ignoreVersionMismatch setting work + // can be removed when we have corresponding ES version + dev: true, + } + ); + + console.log('Starting OpenSearchDashboards server..'); + await root.setup(); + await root.start(); + console.log('Started OpenSearchDashboards server'); + }); + + afterAll(async () => { + // shutdown OpenSearchDashboards server + await root.shutdown(); + }); + + it('delete cache', async () => { + const deleteCacheResponseWrongDataSource = await osdTestServer.request + .delete(root, '/api/v1/configuration/cache') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: 'test' }); + + // Calling clear cache on a datasource that does not exist + expect(deleteCacheResponseWrongDataSource.status).not.toEqual(200); + expect(deleteCacheResponseWrongDataSource.text).toContain( + 'Data Source Error: Saved object [data-source/test] not found' + ); + + const deleteCacheResponseEmptyDataSource = await osdTestServer.request + .delete(root, '/api/v1/configuration/cache') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: '' }); + + // Calling clear cache on an empty datasource calls local cluster + expect(deleteCacheResponseEmptyDataSource.status).toEqual(200); + + const createDataSource = await osdTestServer.request + .post(root, '/api/saved_objects/data-source') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ + attributes: { + title: 'test', + description: '', + endpoint: 'http://localhost:9202', + auth: { + type: 'username_password', + credentials: { + username: 'admin', + password: 'admin', + }, + }, + }, + }); + + const deleteCacheResponseRemoteDataSource = await osdTestServer.request + .delete(root, '/api/v1/configuration/cache') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send({ dataSourceId: createDataSource.body.id }); + + // Calling clear cache on an empty datasource calls local cluster + expect(deleteCacheResponseRemoteDataSource.status).toEqual(200); + }); +});