From 4ba9fcf48935e866a1c32a14207e2e8b860c9181 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 20 Mar 2024 10:52:25 -0400 Subject: [PATCH 01/22] 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); + }); +}); From eae331afbd2533248a66f37b647aecebb5555ed2 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 22 Mar 2024 17:30:39 -0400 Subject: [PATCH 02/22] Lift state up one and generalize flow, render picker for auth tab (#1835) Signed-off-by: Derek Ho --- ...ress-test-multidatasources-enabled-e2e.yml | 46 +- public/apps/account/utils.tsx | 2 +- public/apps/configuration/app-router.tsx | 279 ++++---- .../panels/auth-view/auth-view.tsx | 17 +- .../panels/auth-view/test/auth-view.test.tsx | 5 + .../apps/configuration/panels/get-started.tsx | 20 +- .../__snapshots__/get-started.test.tsx.snap | 14 +- .../panels/test/get-started.test.tsx | 5 + .../__snapshots__/app-router.test.tsx.snap | 643 ++++++++++++++++++ .../configuration/test/app-router.test.tsx | 51 ++ public/apps/configuration/top-nav-menu.tsx | 21 +- .../utils/action-groups-utils.tsx | 8 +- .../utils/audit-logging-utils.tsx | 2 +- .../configuration/utils/auth-view-utils.tsx | 6 +- .../utils/internal-user-detail-utils.tsx | 5 +- .../utils/internal-user-list-utils.tsx | 4 +- .../apps/configuration/utils/request-utils.ts | 30 +- .../configuration/utils/role-detail-utils.tsx | 2 +- .../configuration/utils/role-list-utils.tsx | 6 +- .../utils/tenancy-config_util.tsx | 2 +- .../apps/configuration/utils/tenant-utils.tsx | 6 +- public/utils/auth-info-utils.tsx | 2 +- public/utils/dashboards-info-utils.tsx | 4 +- public/utils/logout-utils.tsx | 2 +- server/routes/index.ts | 71 +- .../multi_datasources_enabled.spec.js | 13 + .../security_entity_api.test.ts | 76 ++- 27 files changed, 1093 insertions(+), 249 deletions(-) create mode 100644 public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap create mode 100644 public/apps/configuration/test/app-router.test.tsx diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml index 5a97342bb..71b01c69b 100644 --- a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -70,6 +70,50 @@ jobs: node.max_local_storage_nodes: 3 EOT + # Add Custom Configuration to differentiate between local and remote cluster + - name: Create Custom Configuration for Linux + if: ${{ runner.os == 'Linux'}} + run: | + echo "Creating new custom configuration" + cat << 'EOT' > config_custom.yml + --- + _meta: + type: "config" + config_version: 2 + config: + dynamic: + http: + anonymous_auth_enabled: false + authc: + basic_internal_auth_domain: + description: "Authenticate via HTTP Basic against internal users database" + http_enabled: true + transport_enabled: true + order: 0 + http_authenticator: + type: basic + challenge: false + authentication_backend: + type: intern + saml_auth_domain: + http_enabled: true + transport_enabled: false + order: 1 + http_authenticator: + type: saml + challenge: true + config: + idp: + entity_id: urn:example:idp + metadata_url: http://localhost:7000/metadata + sp: + entity_id: https://localhost:9200 + kibana_url: http://localhost:5601 + exchange_key: 6aff3042-1327-4f3d-82f0-40a157ac4464 + authentication_backend: + type: noop + EOT + - name: Download security plugin and create setup scripts uses: ./.github/actions/download-plugin with: @@ -84,7 +128,7 @@ jobs: plugins: "file:$(pwd)/opensearch-security.zip" security-enabled: true admin-password: ${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} - security_config_file: ${{ inputs.security_config_file }} + security_config_file: config_custom.yml opensearch_yml_file: remote_opensearch.yml opensearch_port: 9202 diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index 180a2861a..31b5c21e9 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -26,7 +26,7 @@ import { httpGet, httpGetWithIgnores, httpPost } from '../configuration/utils/re import { setShouldShowTenantPopup } from '../../utils/storage-utils'; export function fetchAccountInfo(http: HttpStart): Promise { - return httpGet(http, API_ENDPOINT_ACCOUNT_INFO); + return httpGet({ http, url: API_ENDPOINT_ACCOUNT_INFO }); } export async function fetchAccountInfoSafe(http: HttpStart): Promise { diff --git a/public/apps/configuration/app-router.tsx b/public/apps/configuration/app-router.tsx index e063463f0..ea9ae73c0 100644 --- a/public/apps/configuration/app-router.tsx +++ b/public/apps/configuration/app-router.tsx @@ -15,8 +15,9 @@ import { EuiBreadcrumb, EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; import { flow, partial } from 'lodash'; -import React from 'react'; +import React, { createContext, useState } from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; +import { DataSourceOption } from '../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; import { AppDependencies } from '../types'; import { AuditLogging } from './panels/audit-logging/audit-logging'; import { AuditLoggingEditSettings } from './panels/audit-logging/audit-logging-edit-settings'; @@ -143,140 +144,154 @@ function decodeParams(params: { [k: string]: string }): any { }, {}); } +export interface DataSourceContextType { + dataSource: DataSourceOption; + setDataSource: React.Dispatch>; +} + +export const DataSourceContext = createContext(null); + export function AppRouter(props: AppDependencies) { const setGlobalBreadcrumbs = flow(getBreadcrumbs, props.coreStart.chrome.setBreadcrumbs); + const [dataSource, setDataSource] = useState({ + id: '', + label: 'Local cluster', + checked: 'on', + }); return ( - - - {allNavPanelUrls.map((route) => ( - // Create different routes to update the 'selected' nav item . - - - - - - ))} - - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - { - setGlobalBreadcrumbs(ResourceType.roles); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.auth); - return ; - }} - /> - ( - - )} - /> - { - setGlobalBreadcrumbs(ResourceType.users); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.serviceAccounts); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.auditLogging, 'General settings'); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.auditLogging, 'Compliance settings'); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.auditLogging); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.permissions); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.tenants); - return ; - }} - /> - { - setGlobalBreadcrumbs(ResourceType.tenants); - return ; - }} - /> - { - setGlobalBreadcrumbs(); - return ; - }} - /> - - - - - - + + + + {allNavPanelUrls.map((route) => ( + // Create different routes to update the 'selected' nav item . + + + + + + ))} + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + { + setGlobalBreadcrumbs(ResourceType.roles); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.auth); + return ; + }} + /> + ( + + )} + /> + { + setGlobalBreadcrumbs(ResourceType.users); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.serviceAccounts); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.auditLogging, 'General settings'); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.auditLogging, 'Compliance settings'); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.auditLogging); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.permissions); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.tenants); + return ; + }} + /> + { + setGlobalBreadcrumbs(ResourceType.tenants); + return ; + }} + /> + { + setGlobalBreadcrumbs(); + return ; + }} + /> + + + + + + + ); } diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 80a8651bf..ad0a3dd27 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { EuiPageHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { AuthenticationSequencePanel } from './authentication-sequence-panel'; @@ -23,17 +23,22 @@ import { AppDependencies } from '../../../types'; import { ExternalLinkButton } from '../../utils/display-utils'; import { getSecurityConfig } from '../../utils/auth-view-utils'; import { InstructionView } from './instruction-view'; +import { DataSourceContext } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); const [authorization, setAuthorization] = React.useState([]); const [loading, setLoading] = useState(false); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { const fetchData = async () => { try { setLoading(true); - const config = await getSecurityConfig(props.coreStart.http); + const config = await getSecurityConfig(props.coreStart.http, { + dataSourceId: dataSource.id, + }); setAuthentication(config.authc); setAuthorization(config.authz); @@ -45,7 +50,7 @@ export function AuthView(props: AppDependencies) { }; fetchData(); - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); if (isEmpty(authentication)) { return ; @@ -53,6 +58,12 @@ export function AuthView(props: AppDependencies) { return ( <> +

Authentication and authorization

diff --git a/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx b/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx index 398204969..8a97113ec 100644 --- a/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx +++ b/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx @@ -17,6 +17,11 @@ import { shallow } from 'enzyme'; import React from 'react'; import { AuthView } from '../auth-view'; +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + // eslint-disable-next-line const mockAuthViewUtils = require('../../../utils/auth-view-utils'); diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index a8a082e22..f1fe2149a 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -26,8 +26,9 @@ import { EuiTitle, EuiGlobalToastList, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; +import { DataSourceOption } from '../../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; import { AppDependencies } from '../../types'; import { buildHashUrl } from '../utils/url-builder'; import { Action } from '../types'; @@ -37,7 +38,7 @@ 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'; +import { DataSourceContext } from '../app-router'; const addBackendStep = { title: 'Add backends', @@ -160,7 +161,7 @@ const setOfSteps = [ }, ]; -export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: Cluster) { +export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: DataSourceOption) { if (dataSourceEnabled) { return `for ${cluster.label || 'Local cluster'}`; } @@ -169,7 +170,7 @@ export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: Clu export function GetStarted(props: AppDependencies) { const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; - const [dataSource, setDataSource] = useState({ id: '', label: '' }); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; let steps; if (props.config.ui.backend_configurable) { @@ -185,7 +186,8 @@ export function GetStarted(props: AppDependencies) { @@ -253,8 +255,12 @@ export function GetStarted(props: AppDependencies) { data-test-subj="purge-cache" onClick={async () => { try { - await httpDelete(props.coreStart.http, API_ENDPOINT_CACHE, { - dataSourceId: dataSource.id, + await httpDelete({ + http: props.coreStart.http, + url: API_ENDPOINT_CACHE, + query: { + dataSourceId: dataSource.id, + }, }); addToast( createSuccessToast( 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 81d2bfb1d..f615b622b 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 @@ -21,7 +21,12 @@ exports[`Get started (landing page) renders when backend configuration is disabl dataSourcePickerReadOnly={false} depsStart={Object {}} params={Object {}} - setDatasourceId={[Function]} + selectedDataSource={ + Object { + "id": "test", + } + } + setDataSource={[MockFunction]} /> ({ httpDelete: jest.fn(), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + describe('Get started (landing page)', () => { const mockCoreStart = { http: 1, diff --git a/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap new file mode 100644 index 000000000..68026a1c1 --- /dev/null +++ b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap @@ -0,0 +1,643 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enabled 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/public/apps/configuration/test/app-router.test.tsx b/public/apps/configuration/test/app-router.test.tsx new file mode 100644 index 000000000..990c448a3 --- /dev/null +++ b/public/apps/configuration/test/app-router.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { shallow } from 'enzyme'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; +import { AppRouter } from '../app-router'; + +describe('SecurityPluginTopNavMenu', () => { + const coreStartMock = { + savedObjects: { + client: jest.fn(), + }, + notifications: jest.fn(), + chrome: { + setBreadcrumbs: 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 = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/apps/configuration/top-nav-menu.tsx b/public/apps/configuration/top-nav-menu.tsx index b36d46e6e..ec7e2641a 100644 --- a/public/apps/configuration/top-nav-menu.tsx +++ b/public/apps/configuration/top-nav-menu.tsx @@ -14,20 +14,25 @@ */ import React from 'react'; -import { CoreStart } from 'opensearch-dashboards/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public/types'; -import { ClientConfigType } from '../../types'; +import { DataSourceOption } from '../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; import { PLUGIN_NAME } from '../../../common'; import { AppDependencies } from '../types'; -import { Cluster } from '../../types'; export interface TopNavMenuProps extends AppDependencies { dataSourcePickerReadOnly: boolean; - setDatasourceId: React.Dispatch>; + setDataSource: React.Dispatch>; + selectedDataSource: DataSourceOption; } export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { - const { coreStart, depsStart, params, dataSourceManagement, setDatasourceId } = props; + const { + coreStart, + depsStart, + params, + dataSourceManagement, + setDataSource, + selectedDataSource, + } = props; const { setHeaderActionMenu } = params; const DataSourceMenu = dataSourceManagement?.ui.DataSourceMenu; const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; @@ -39,7 +44,9 @@ export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { savedObjects={coreStart.savedObjects.client} setMenuMountPoint={setHeaderActionMenu} notifications={coreStart.notifications} - dataSourceCallBackFunc={(datasource) => setDatasourceId(datasource)} + dataSourceCallBackFunc={setDataSource} + // Single select for now + selectedOption={[selectedDataSource]} hideLocalCluster={false} fullWidth={false} /> diff --git a/public/apps/configuration/utils/action-groups-utils.tsx b/public/apps/configuration/utils/action-groups-utils.tsx index 7d565b01f..b832f0b50 100644 --- a/public/apps/configuration/utils/action-groups-utils.tsx +++ b/public/apps/configuration/utils/action-groups-utils.tsx @@ -30,10 +30,10 @@ export interface PermissionListingItem { } export async function fetchActionGroups(http: HttpStart): Promise> { - const actiongroups = await httpGet>( + const actiongroups = await httpGet>({ http, - API_ENDPOINT_ACTIONGROUPS - ); + url: API_ENDPOINT_ACTIONGROUPS, + }); return actiongroups.data; } @@ -98,6 +98,6 @@ export async function updateActionGroup( export async function requestDeleteActionGroups(http: HttpStart, groups: string[]) { for (const group of groups) { - await httpDelete(http, getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group)); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group) }); } } diff --git a/public/apps/configuration/utils/audit-logging-utils.tsx b/public/apps/configuration/utils/audit-logging-utils.tsx index ecc650965..1af24e552 100644 --- a/public/apps/configuration/utils/audit-logging-utils.tsx +++ b/public/apps/configuration/utils/audit-logging-utils.tsx @@ -23,6 +23,6 @@ export async function updateAuditLogging(http: HttpStart, updateObject: AuditLog } export async function getAuditLogging(http: HttpStart): Promise { - const rawConfiguration = await httpGet(http, API_ENDPOINT_AUDITLOGGING); + const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_AUDITLOGGING }); return rawConfiguration?.config; } diff --git a/public/apps/configuration/utils/auth-view-utils.tsx b/public/apps/configuration/utils/auth-view-utils.tsx index 147387f39..4a4d168e8 100644 --- a/public/apps/configuration/utils/auth-view-utils.tsx +++ b/public/apps/configuration/utils/auth-view-utils.tsx @@ -13,11 +13,11 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_SECURITYCONFIG } from '../constants'; import { httpGet } from './request-utils'; -export async function getSecurityConfig(http: HttpStart) { - const rawSecurityConfig = await httpGet(http, API_ENDPOINT_SECURITYCONFIG); +export async function getSecurityConfig(http: HttpStart, query?: HttpFetchQuery) { + const rawSecurityConfig = await httpGet({ http, url: API_ENDPOINT_SECURITYCONFIG, query }); return rawSecurityConfig.data.config.dynamic; } diff --git a/public/apps/configuration/utils/internal-user-detail-utils.tsx b/public/apps/configuration/utils/internal-user-detail-utils.tsx index 6293cab59..b838e2171 100644 --- a/public/apps/configuration/utils/internal-user-detail-utils.tsx +++ b/public/apps/configuration/utils/internal-user-detail-utils.tsx @@ -20,7 +20,10 @@ import { httpGet, httpPost } from './request-utils'; import { getResourceUrl } from './resource-utils'; export async function getUserDetail(http: HttpStart, username: string): Promise { - return await httpGet(http, getResourceUrl(API_ENDPOINT_INTERNALUSERS, username)); + return await httpGet({ + http, + url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), + }); } export async function updateUser( diff --git a/public/apps/configuration/utils/internal-user-list-utils.tsx b/public/apps/configuration/utils/internal-user-list-utils.tsx index 19b0488c7..9308734e1 100644 --- a/public/apps/configuration/utils/internal-user-list-utils.tsx +++ b/public/apps/configuration/utils/internal-user-list-utils.tsx @@ -39,7 +39,7 @@ export function transformUserData(rawData: DataObject): InternalUs export async function requestDeleteUsers(http: HttpStart, users: string[]) { for (const user of users) { - await httpDelete(http, getResourceUrl(API_ENDPOINT_INTERNALUSERS, user)); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user) }); } } @@ -52,7 +52,7 @@ async function getUserListRaw( ENDPOINT = API_ENDPOINT_SERVICEACCOUNTS; } - return await httpGet>(http, ENDPOINT); + return await httpGet>({ http, url: ENDPOINT }); } export async function getUserList( diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index 20502309f..a5e7099a3 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -13,17 +13,30 @@ * permissions and limitations under the License. */ -import { HttpStart, HttpHandler } from 'opensearch-dashboards/public'; +import { HttpStart, HttpHandler, HttpFetchQuery } from 'opensearch-dashboards/public'; -export async function request(requestFunc: HttpHandler, url: string, body?: object): Promise { +interface RequestType { + http: HttpStart; + url: string; + body?: object; + query?: HttpFetchQuery; +} + +export async function request( + requestFunc: HttpHandler, + url: string, + body?: object, + query?: HttpFetchQuery +): Promise { if (body) { - return (await requestFunc(url, { body: JSON.stringify(body) })) as T; + return (await requestFunc(url, { body: JSON.stringify(body), query })) as T; } - return (await requestFunc(url)) as T; + return (await requestFunc(url, { query })) as T; } -export async function httpGet(http: HttpStart, url: string): Promise { - return await request(http.get, url); +export async function httpGet(params: RequestType): Promise { + const { http, url, body, query } = params; + return await request(http.get, url, body, query); } export async function httpPost(http: HttpStart, url: string, body?: object): Promise { @@ -34,8 +47,9 @@ 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, body?: object): Promise { - return await request(http.delete, url, body); +export async function httpDelete(params: RequestType): Promise { + const { http, url, body, query } = params; + return await request(http.delete, url, body, query); } /** diff --git a/public/apps/configuration/utils/role-detail-utils.tsx b/public/apps/configuration/utils/role-detail-utils.tsx index c2525fb90..bc8ca142e 100644 --- a/public/apps/configuration/utils/role-detail-utils.tsx +++ b/public/apps/configuration/utils/role-detail-utils.tsx @@ -20,7 +20,7 @@ import { httpGet, httpPost } from './request-utils'; import { getResourceUrl } from './resource-utils'; export async function getRoleDetail(http: HttpStart, roleName: string): Promise { - return await httpGet(http, getResourceUrl(API_ENDPOINT_ROLES, roleName)); + return await httpGet({ http, url: getResourceUrl(API_ENDPOINT_ROLES, roleName) }); } export async function updateRole(http: HttpStart, roleName: string, updateObject: RoleUpdate) { diff --git a/public/apps/configuration/utils/role-list-utils.tsx b/public/apps/configuration/utils/role-list-utils.tsx index edc44d57d..7eefd9c69 100644 --- a/public/apps/configuration/utils/role-list-utils.tsx +++ b/public/apps/configuration/utils/role-list-utils.tsx @@ -94,17 +94,17 @@ export function buildSearchFilterOptions(roleList: any[], attrName: string): Arr // Submit request to delete given roles. No error handling in this function. export async function requestDeleteRoles(http: HttpStart, roles: string[]) { for (const role of roles) { - await httpDelete(http, getResourceUrl(API_ENDPOINT_ROLES, role)); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ROLES, role) }); await httpDeleteWithIgnores(http, getResourceUrl(API_ENDPOINT_ROLESMAPPING, role), [404]); } } // TODO: have a type definition for it export function fetchRole(http: HttpStart): Promise { - return httpGet(http, API_ENDPOINT_ROLES); + return httpGet({ http, url: API_ENDPOINT_ROLES }); } // TODO: have a type definition for it export function fetchRoleMapping(http: HttpStart): Promise { - return httpGet(http, API_ENDPOINT_ROLESMAPPING); + return httpGet({ http, url: API_ENDPOINT_ROLESMAPPING }); } diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index 215e44622..8850a405c 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -23,6 +23,6 @@ export async function updateTenancyConfig(http: HttpStart, updateObject: Tenancy } export async function getTenancyConfig(http: HttpStart): Promise { - const rawConfiguration = await httpGet(http, API_ENDPOINT_TENANCY_CONFIGS); + const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_TENANCY_CONFIGS }); return rawConfiguration; } diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index 2aee7c355..2fa0772b8 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -64,7 +64,7 @@ export const PRIVATE_USER_DICT: { [key: string]: string } = { }; export async function fetchTenants(http: HttpStart): Promise> { - return (await httpGet>(http, API_ENDPOINT_TENANTS)).data; + return (await httpGet>({ http, url: API_ENDPOINT_TENANTS })).data; } export async function fetchTenantNameList(http: HttpStart): Promise { @@ -89,7 +89,7 @@ export function transformTenantData(rawTenantData: DataObject): Tenant[] } export async function fetchCurrentTenant(http: HttpStart): Promise { - return await httpGet(http, API_ENDPOINT_MULTITENANCY); + return await httpGet({ http, url: API_ENDPOINT_MULTITENANCY }); } export async function updateTenant( @@ -111,7 +111,7 @@ export async function updateTenancyConfiguration( export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { for (const tenant of tenants) { - await httpDelete(http, getResourceUrl(API_ENDPOINT_TENANTS, tenant)); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenant) }); } } diff --git a/public/utils/auth-info-utils.tsx b/public/utils/auth-info-utils.tsx index f91dd2299..c766c7bce 100644 --- a/public/utils/auth-info-utils.tsx +++ b/public/utils/auth-info-utils.tsx @@ -19,7 +19,7 @@ import { httpGet } from '../apps/configuration/utils/request-utils'; import { AuthInfo } from '../types'; export async function getAuthInfo(http: HttpStart) { - return await httpGet(http, API_ENDPOINT_AUTHINFO); + return await httpGet({ http, url: API_ENDPOINT_AUTHINFO }); } export async function getCurrentUser(http: HttpStart) { diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index 55804eb04..d54897122 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -17,11 +17,9 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_DASHBOARDSINFO } from '../../common'; import { httpGet, httpGetWithIgnores } from '../apps/configuration/utils/request-utils'; import { DashboardsInfo } from '../types'; -import { AccountInfo } from '../apps/account/types'; -import { API_ENDPOINT_ACCOUNT_INFO } from '../apps/account/constants'; export async function getDashboardsInfo(http: HttpStart) { - return await httpGet(http, API_ENDPOINT_DASHBOARDSINFO); + return await httpGet({ http, url: API_ENDPOINT_DASHBOARDSINFO }); } export async function getDashboardsInfoSafe(http: HttpStart): Promise { diff --git a/public/utils/logout-utils.tsx b/public/utils/logout-utils.tsx index a0cb6529a..90f5a75a0 100644 --- a/public/utils/logout-utils.tsx +++ b/public/utils/logout-utils.tsx @@ -45,5 +45,5 @@ export function interceptError(logoutUrl: string, thisWindow: Window): any { } export async function fetchCurrentAuthType(http: HttpStart): Promise { - return await httpGet(http, API_ENDPOINT_AUTHTYPE); + return await httpGet({ http, url: API_ENDPOINT_AUTHTYPE }); } diff --git a/server/routes/index.ts b/server/routes/index.ts index 22da821d3..7668d358b 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -241,6 +241,9 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { params: schema.object({ resourceName: schema.string(), }), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async ( @@ -256,9 +259,13 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { } else if (request.params.resourceName === 'internalaccounts') { esResp = await client.callAsCurrentUser('opensearch_security.listInternalAccounts'); } else { - esResp = await client.callAsCurrentUser('opensearch_security.listResource', { - resourceName: request.params.resourceName, - }); + esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.listResource', + { resourceName: request.params.resourceName } + ); } return response.ok({ body: { @@ -748,19 +755,27 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { { path: `${API_PREFIX}/configuration/cache`, validate: { - body: schema.object({ + query: schema.object({ dataSourceId: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { - return wrapRouteWithDataSource( - dataSourceEnabled, - context, - request, - response, - 'opensearch_security.clearCache' - ); + try { + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.clearCache' + ); + return response.ok({ + body: { + message: esResp.message, + }, + }); + } catch (error) { + return errorResponse(response, error); + } } ); @@ -887,36 +902,16 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { const wrapRouteWithDataSource = async ( dataSourceEnabled: boolean, context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - endpoint: string + request: OpenSearchDashboardsRequest, + endpoint: string, + body?: Record ) => { - if (!dataSourceEnabled || !request.body?.dataSourceId) { + if (!dataSourceEnabled || !request.query?.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); - } + return await client.callAsCurrentUser(endpoint, body); } 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); - } + const client = context.dataSource.opensearch.legacy.getClient(request.query?.dataSourceId); + return await client.callAPI(endpoint, body); } }; diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 35de36c36..3f21904d4 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -65,5 +65,18 @@ describe('Multi-datasources enabled', () => { cy.contains('li.euiSelectableListItem', '9202').click(); cy.get('[data-test-subj="purge-cache"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); + // Data source persisted across tabs + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); + }); + + it('Checks Auth Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); + // Local cluster auth + cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); + // Remote cluster auth + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); }); }); diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index e179cd136..d584882f6 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -425,6 +425,7 @@ describe('start OpenSearch Dashboards server', () => { describe('start OpenSearch Dashboards server multi datasources enabled', () => { let root: Root; + let dataSourceId: string; beforeAll(async () => { root = osdTestServer.createRootWithSettings( @@ -455,6 +456,24 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { await root.setup(); await root.start(); console.log('Started OpenSearchDashboards server'); + 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', + }, + }, + }, + }); + dataSourceId = createDataSource.body.id; }); afterAll(async () => { @@ -464,9 +483,8 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { it('delete cache', async () => { const deleteCacheResponseWrongDataSource = await osdTestServer.request - .delete(root, '/api/v1/configuration/cache') - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) - .send({ dataSourceId: 'test' }); + .delete(root, '/api/v1/configuration/cache?dataSourceId=test') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); // Calling clear cache on a datasource that does not exist expect(deleteCacheResponseWrongDataSource.status).not.toEqual(200); @@ -476,36 +494,42 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { const deleteCacheResponseEmptyDataSource = await osdTestServer.request .delete(root, '/api/v1/configuration/cache') - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) - .send({ dataSourceId: '' }); + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); // 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 }); + .delete(root, `/api/v1/configuration/cache?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); // Calling clear cache on an empty datasource calls local cluster expect(deleteCacheResponseRemoteDataSource.status).toEqual(200); }); + + it('Gets auth page correctly', async () => { + const getAuthResponseWrongDataSource = await osdTestServer.request + .get(root, '/api/v1/configuration/securityconfig?dataSourceId=test') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + + // Getting auth info on a datasource that does not exist + expect(getAuthResponseWrongDataSource.status).not.toEqual(200); + expect(getAuthResponseWrongDataSource.text).toContain( + 'Data Source Error: Saved object [data-source/test] not found' + ); + + const getAuthResponseEmptyDataSource = await osdTestServer.request + .get(root, '/api/v1/configuration/securityconfig?dataSourceId=') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + + // Getting auth info on an empty datasource calls local cluster + expect(getAuthResponseEmptyDataSource.status).toEqual(200); + + const getAuthResponseRemoteDataSource = await osdTestServer.request + .get(root, `/api/v1/configuration/securityconfig?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + + // Getting auth info on an empty datasource calls local cluster + expect(getAuthResponseRemoteDataSource.status).toEqual(200); + }); }); From 69319fe40fde826699c2139921ef10b433dcc036 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 27 Mar 2024 13:51:54 -0400 Subject: [PATCH 03/22] Add multi datasource support for the Users tab (#1848) * Add multi datasource support for internal users tab Signed-off-by: Derek Ho * Add test coverage for front end and backend Signed-off-by: Derek Ho * Push cypress test for users Signed-off-by: Derek Ho * Remove extra click Signed-off-by: Derek Ho * Consume changes in core Signed-off-by: Derek Ho * Don't rely on public export for type Signed-off-by: Derek Ho * Consume list changes, fix cypress test unskips auth test Signed-off-by: Derek Ho * Fix cypress test Signed-off-by: Derek Ho * Fix test Signed-off-by: Derek Ho * Test fix Signed-off-by: Derek Ho * Add timeout as well Signed-off-by: Derek Ho * Try different way to select the option Signed-off-by: Derek Ho * Fix test Signed-off-by: Derek Ho * Use data test subj for more consistency Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- public/apps/account/utils.tsx | 12 +- .../panels/auth-view/auth-view.tsx | 8 +- .../apps/configuration/panels/get-started.tsx | 12 +- .../internal-user-edit/internal-user-edit.tsx | 35 +++++- .../test/internal-user-edit.test.tsx | 8 +- .../panels/test/get-started.test.tsx | 6 - .../panels/test/user-list.test.tsx | 4 + .../apps/configuration/panels/user-list.tsx | 32 +++++- .../configuration/test/top-nav-menu.test.tsx | 2 +- public/apps/configuration/top-nav-menu.tsx | 27 +++-- .../utils/action-groups-utils.tsx | 6 +- .../utils/audit-logging-utils.tsx | 2 +- .../utils/internal-user-detail-utils.tsx | 19 +++- .../utils/internal-user-list-utils.tsx | 18 +-- .../apps/configuration/utils/request-utils.ts | 5 +- .../configuration/utils/role-detail-utils.tsx | 6 +- .../utils/role-mapping-utils.tsx | 6 +- .../utils/tenancy-config_util.tsx | 2 +- .../apps/configuration/utils/tenant-utils.tsx | 8 +- .../test/internal-user-list-utils.test.tsx | 106 +++++++++++++++++- public/utils/datasource-utils.ts | 27 +++++ public/utils/login-utils.tsx | 10 +- public/utils/test/datasource-utils.test.ts | 28 +++++ server/routes/index.ts | 85 +++++++++----- .../multi_datasources_enabled.spec.js | 52 ++++++++- test/helper/entity_operation.ts | 24 ++++ .../security_entity_api.test.ts | 80 ++++++++++++- 27 files changed, 527 insertions(+), 103 deletions(-) create mode 100644 public/utils/datasource-utils.ts create mode 100644 public/utils/test/datasource-utils.test.ts diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index 31b5c21e9..df3c480be 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -34,7 +34,7 @@ export async function fetchAccountInfoSafe(http: HttpStart): Promise { - await httpPost(http, API_AUTH_LOGOUT); + await httpPost({ http, url: API_AUTH_LOGOUT }); setShouldShowTenantPopup(null); // Clear everything in the sessionStorage since they can contain sensitive information sessionStorage.clear(); @@ -70,8 +70,12 @@ export async function updateNewPassword( newPassword: string, currentPassword: string ): Promise { - await httpPost(http, API_ENDPOINT_ACCOUNT_INFO, { - password: newPassword, - current_password: currentPassword, + await httpPost({ + http, + url: API_ENDPOINT_ACCOUNT_INFO, + body: { + password: newPassword, + current_password: currentPassword, + }, }); } diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index ad0a3dd27..eec379ead 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -25,6 +25,7 @@ import { getSecurityConfig } from '../../utils/auth-view-utils'; import { InstructionView } from './instruction-view'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); @@ -36,9 +37,10 @@ export function AuthView(props: AppDependencies) { const fetchData = async () => { try { setLoading(true); - const config = await getSecurityConfig(props.coreStart.http, { - dataSourceId: dataSource.id, - }); + const config = await getSecurityConfig( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setAuthentication(config.authc); setAuthorization(config.authz); diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index f1fe2149a..d55c8aefa 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -39,6 +39,7 @@ import { httpDelete } from '../utils/request-utils'; import { createSuccessToast, createUnknownErrorToast, useToastState } from '../utils/toast-utils'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { DataSourceContext } from '../app-router'; +import { getClusterInfoIfEnabled, createDataSourceQuery } from '../../../utils/datasource-utils'; const addBackendStep = { title: 'Add backends', @@ -161,13 +162,6 @@ const setOfSteps = [ }, ]; -export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: DataSourceOption) { - if (dataSourceEnabled) { - return `for ${cluster.label || 'Local cluster'}`; - } - return ''; -} - export function GetStarted(props: AppDependencies) { const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; @@ -258,9 +252,7 @@ export function GetStarted(props: AppDependencies) { await httpDelete({ http: props.coreStart.http, url: API_ENDPOINT_CACHE, - query: { - dataSourceId: dataSource.id, - }, + query: createDataSourceQuery(dataSource.id), }); addToast( createSuccessToast( diff --git a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx index 1237ef9f3..b7bebe658 100644 --- a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx +++ b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx @@ -24,7 +24,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { BreadcrumbsPageDependencies } from '../../../types'; import { InternalUserUpdate } from '../../types'; import { ResourceType } from '../../../../../common'; @@ -47,6 +47,9 @@ import { NameRow } from '../../utils/name-row'; import { DocLinks } from '../../constants'; import { constructErrorMessageAndLog } from '../../../error-utils'; import { BackendRolePanel } from './backend-role-panel'; +import { DataSourceContext } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface InternalUserEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -72,13 +75,18 @@ export function InternalUserEdit(props: InternalUserEditDeps) { const [toasts, addToast, removeToast] = useToastState(); const [isFormValid, setIsFormValid] = useState(true); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { const action = props.action; if (action === 'edit' || action === 'duplicate') { const fetchData = async () => { try { - const user = await getUserDetail(props.coreStart.http, props.sourceUserName); + const user = await getUserDetail( + props.coreStart.http, + props.sourceUserName, + createDataSourceQuery(dataSource.id) + ); setAttributes(buildAttributeState(user.attributes)); setBackendRoles(user.backend_roles); setUserName(generateResourceName(action, props.sourceUserName)); @@ -90,7 +98,7 @@ export function InternalUserEdit(props: InternalUserEditDeps) { fetchData(); } - }, [addToast, props.action, props.coreStart.http, props.sourceUserName]); + }, [addToast, props.action, props.coreStart.http, props.sourceUserName, dataSource.id]); const updateUserHandler = async () => { try { @@ -117,7 +125,12 @@ export function InternalUserEdit(props: InternalUserEditDeps) { updateObject.password = password; } - await updateUser(props.coreStart.http, userName, updateObject); + await updateUser( + props.coreStart.http, + userName, + updateObject, + createDataSourceQuery(dataSource.id) + ); setCrossPageToast(buildUrl(ResourceType.users), { id: 'updateUserSucceeded', @@ -135,6 +148,12 @@ export function InternalUserEdit(props: InternalUserEditDeps) { return ( <> + {props.buildBreadcrumbs(TITLE_TEXT_DICT[props.action])} @@ -187,7 +206,13 @@ export function InternalUserEdit(props: InternalUserEditDeps) { - + {props.action === 'edit' ? 'Save changes' : 'Create'} diff --git a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx index d8d5efc5d..8dcc630d8 100644 --- a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx +++ b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx @@ -31,6 +31,10 @@ jest.mock('../../../utils/toast-utils', () => ({ createUnknownErrorToast: jest.fn(), useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); describe('Internal user edit', () => { const sampleUsername = 'user1'; @@ -79,7 +83,9 @@ describe('Internal user edit', () => { /> ); - expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername); + expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername, { + dataSourceId: 'test', + }); }); it('should not submit if password is empty on creation', () => { diff --git a/public/apps/configuration/panels/test/get-started.test.tsx b/public/apps/configuration/panels/test/get-started.test.tsx index bbe648a6c..09e64578d 100644 --- a/public/apps/configuration/panels/test/get-started.test.tsx +++ b/public/apps/configuration/panels/test/get-started.test.tsx @@ -175,11 +175,5 @@ 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/panels/test/user-list.test.tsx b/public/apps/configuration/panels/test/user-list.test.tsx index 32455fa53..2e96573f5 100644 --- a/public/apps/configuration/panels/test/user-list.test.tsx +++ b/public/apps/configuration/panels/test/user-list.test.tsx @@ -37,6 +37,10 @@ jest.mock('../../utils/context-menu', () => ({ .fn() .mockImplementation((buttonText, buttonProps, children) => [children, jest.fn()]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); import { getAuthInfo } from '../../../../utils/auth-info-utils'; import { buildHashUrl } from '../../utils/url-builder'; diff --git a/public/apps/configuration/panels/user-list.tsx b/public/apps/configuration/panels/user-list.tsx index b75f62283..eef98c64b 100644 --- a/public/apps/configuration/panels/user-list.tsx +++ b/public/apps/configuration/panels/user-list.tsx @@ -31,7 +31,7 @@ import { Query, } from '@elastic/eui'; import { Dictionary, difference, isEmpty, map } from 'lodash'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { getAuthInfo } from '../../../utils/auth-info-utils'; import { AppDependencies } from '../../types'; import { API_ENDPOINT_INTERNALUSERS, DocLinks } from '../constants'; @@ -48,6 +48,9 @@ import { } from '../utils/internal-user-list-utils'; import { showTableStatusMessage } from '../utils/loading-spinner-utils'; import { buildHashUrl } from '../utils/url-builder'; +import { DataSourceContext } from '../app-router'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; +import { createDataSourceQuery } from '../../../utils/datasource-utils'; export function dictView(items: Dictionary) { if (isEmpty(items)) { @@ -103,12 +106,17 @@ export function UserList(props: AppDependencies) { const [currentUsername, setCurrentUsername] = useState(''); const [loading, setLoading] = useState(false); const [query, setQuery] = useState(null); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { const fetchData = async () => { try { setLoading(true); - const userDataPromise = getUserList(props.coreStart.http, ResourceType.users); + const userDataPromise = getUserList( + props.coreStart.http, + ResourceType.users, + createDataSourceQuery(dataSource.id) + ); setCurrentUsername((await getAuthInfo(props.coreStart.http)).user_name); setUserData(await userDataPromise); } catch (e) { @@ -120,12 +128,16 @@ export function UserList(props: AppDependencies) { }; fetchData(); - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); const handleDelete = async () => { const usersToDelete: string[] = selection.map((r) => r.username); try { - await requestDeleteUsers(props.coreStart.http, usersToDelete); + await requestDeleteUsers( + props.coreStart.http, + usersToDelete, + createDataSourceQuery(dataSource.id) + ); // Refresh from server (calling fetchData) does not work here, the server still return the users // that had been just deleted, probably because ES takes some time to sync to all nodes. // So here remove the selected users from local memory directly. @@ -194,6 +206,12 @@ export function UserList(props: AppDependencies) { return ( <> +

Internal users

@@ -224,7 +242,11 @@ export function UserList(props: AppDependencies) { {actionsMenu} - + Create user account diff --git a/public/apps/configuration/test/top-nav-menu.test.tsx b/public/apps/configuration/test/top-nav-menu.test.tsx index 93fef3d80..2522d7a60 100644 --- a/public/apps/configuration/test/top-nav-menu.test.tsx +++ b/public/apps/configuration/test/top-nav-menu.test.tsx @@ -29,7 +29,7 @@ describe('SecurityPluginTopNavMenu', () => { const dataSourceManagementMock = { ui: { - DataSourceMenu: dataSourceMenuMock, + getDataSourceMenu: jest.fn().mockReturnValue(dataSourceMenuMock), }, }; diff --git a/public/apps/configuration/top-nav-menu.tsx b/public/apps/configuration/top-nav-menu.tsx index ec7e2641a..ce2dfb1a0 100644 --- a/public/apps/configuration/top-nav-menu.tsx +++ b/public/apps/configuration/top-nav-menu.tsx @@ -14,7 +14,8 @@ */ import React from 'react'; -import { DataSourceOption } from '../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; +import { DataSourceSelectableConfig } from 'src/plugins/data_source_management/public'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; import { PLUGIN_NAME } from '../../../common'; import { AppDependencies } from '../types'; @@ -32,23 +33,27 @@ export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { dataSourceManagement, setDataSource, selectedDataSource, + dataSourcePickerReadOnly, } = props; const { setHeaderActionMenu } = params; - const DataSourceMenu = dataSourceManagement?.ui.DataSourceMenu; + const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu(); + const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; return dataSourceEnabled ? ( { + // single select for now + setDataSource(dataSources[0]); + }, + fullWidth: true, + }} /> ) : null; }; diff --git a/public/apps/configuration/utils/action-groups-utils.tsx b/public/apps/configuration/utils/action-groups-utils.tsx index b832f0b50..048448ead 100644 --- a/public/apps/configuration/utils/action-groups-utils.tsx +++ b/public/apps/configuration/utils/action-groups-utils.tsx @@ -93,7 +93,11 @@ export async function updateActionGroup( groupName: string, updateObject: ActionGroupUpdate ): Promise { - return await httpPost(http, getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), updateObject); + return await httpPost({ + http, + url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), + body: updateObject, + }); } export async function requestDeleteActionGroups(http: HttpStart, groups: string[]) { diff --git a/public/apps/configuration/utils/audit-logging-utils.tsx b/public/apps/configuration/utils/audit-logging-utils.tsx index 1af24e552..72b04771d 100644 --- a/public/apps/configuration/utils/audit-logging-utils.tsx +++ b/public/apps/configuration/utils/audit-logging-utils.tsx @@ -19,7 +19,7 @@ import { API_ENDPOINT_AUDITLOGGING, API_ENDPOINT_AUDITLOGGING_UPDATE } from '../ import { httpGet, httpPost } from './request-utils'; export async function updateAuditLogging(http: HttpStart, updateObject: AuditLoggingSettings) { - return await httpPost(http, API_ENDPOINT_AUDITLOGGING_UPDATE, updateObject); + return await httpPost({ http, url: API_ENDPOINT_AUDITLOGGING_UPDATE, body: updateObject }); } export async function getAuditLogging(http: HttpStart): Promise { diff --git a/public/apps/configuration/utils/internal-user-detail-utils.tsx b/public/apps/configuration/utils/internal-user-detail-utils.tsx index b838e2171..d51ad526a 100644 --- a/public/apps/configuration/utils/internal-user-detail-utils.tsx +++ b/public/apps/configuration/utils/internal-user-detail-utils.tsx @@ -13,23 +13,34 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_INTERNALUSERS } from '../constants'; import { InternalUser, InternalUserUpdate } from '../types'; import { httpGet, httpPost } from './request-utils'; import { getResourceUrl } from './resource-utils'; -export async function getUserDetail(http: HttpStart, username: string): Promise { +export async function getUserDetail( + http: HttpStart, + username: string, + query: HttpFetchQuery +): Promise { return await httpGet({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), + query, }); } export async function updateUser( http: HttpStart, username: string, - updateObject: InternalUserUpdate + updateObject: InternalUserUpdate, + query: HttpFetchQuery ): Promise { - return await httpPost(http, getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), updateObject); + return await httpPost({ + http, + url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), + body: updateObject, + query, + }); } diff --git a/public/apps/configuration/utils/internal-user-list-utils.tsx b/public/apps/configuration/utils/internal-user-list-utils.tsx index 9308734e1..5f8ed3470 100644 --- a/public/apps/configuration/utils/internal-user-list-utils.tsx +++ b/public/apps/configuration/utils/internal-user-list-utils.tsx @@ -14,7 +14,7 @@ */ import { map } from 'lodash'; -import { HttpStart } from '../../../../../../src/core/public'; +import { HttpFetchQuery, HttpStart } from '../../../../../../src/core/public'; import { API_ENDPOINT_INTERNALACCOUNTS, API_ENDPOINT_INTERNALUSERS, @@ -37,29 +37,31 @@ export function transformUserData(rawData: DataObject): InternalUs })); } -export async function requestDeleteUsers(http: HttpStart, users: string[]) { +export async function requestDeleteUsers(http: HttpStart, users: string[], query: HttpFetchQuery) { for (const user of users) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user) }); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user), query }); } } -async function getUserListRaw( +export async function getUserListRaw( http: HttpStart, - userType: string + userType: string, + query?: HttpFetchQuery ): Promise> { let ENDPOINT = API_ENDPOINT_INTERNALACCOUNTS; if (userType === ResourceType.serviceAccounts) { ENDPOINT = API_ENDPOINT_SERVICEACCOUNTS; } - return await httpGet>({ http, url: ENDPOINT }); + return await httpGet>({ http, url: ENDPOINT, query }); } export async function getUserList( http: HttpStart, - userType: string + userType: string, + query?: HttpFetchQuery ): Promise { - const rawData = await getUserListRaw(http, userType); + const rawData = await getUserListRaw(http, userType, query); return transformUserData(rawData.data); } diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index a5e7099a3..b3cb2110b 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -39,8 +39,9 @@ export async function httpGet(params: RequestType): Promise { return await request(http.get, url, body, query); } -export async function httpPost(http: HttpStart, url: string, body?: object): Promise { - return await request(http.post, url, body); +export async function httpPost(params: RequestType): Promise { + const { http, url, body, query } = params; + return await request(http.post, url, body, query); } export async function httpPut(http: HttpStart, url: string, body?: object): Promise { diff --git a/public/apps/configuration/utils/role-detail-utils.tsx b/public/apps/configuration/utils/role-detail-utils.tsx index bc8ca142e..3686ece7d 100644 --- a/public/apps/configuration/utils/role-detail-utils.tsx +++ b/public/apps/configuration/utils/role-detail-utils.tsx @@ -24,5 +24,9 @@ export async function getRoleDetail(http: HttpStart, roleName: string): Promise< } export async function updateRole(http: HttpStart, roleName: string, updateObject: RoleUpdate) { - return await httpPost(http, getResourceUrl(API_ENDPOINT_ROLES, roleName), updateObject); + return await httpPost({ + http, + url: getResourceUrl(API_ENDPOINT_ROLES, roleName), + body: updateObject, + }); } diff --git a/public/apps/configuration/utils/role-mapping-utils.tsx b/public/apps/configuration/utils/role-mapping-utils.tsx index 049265982..426539fab 100644 --- a/public/apps/configuration/utils/role-mapping-utils.tsx +++ b/public/apps/configuration/utils/role-mapping-utils.tsx @@ -57,5 +57,9 @@ export async function updateRoleMapping( roleName: string, updateObject: RoleMappingDetail ) { - return await httpPost(http, getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), updateObject); + return await httpPost({ + http, + url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), + body: updateObject, + }); } diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index 8850a405c..70ab84c63 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -19,7 +19,7 @@ import { httpGet, httpPut, httpPost } from './request-utils'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; export async function updateTenancyConfig(http: HttpStart, updateObject: TenancyConfigSettings) { - return await httpPost(http, API_ENDPOINT_TENANCY_CONFIGS, updateObject); + return await httpPost({ http, url: API_ENDPOINT_TENANCY_CONFIGS, body: updateObject }); } export async function getTenancyConfig(http: HttpStart): Promise { diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index 2fa0772b8..2aa1309dc 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -97,7 +97,11 @@ export async function updateTenant( tenantName: string, updateObject: TenantUpdate ) { - return await httpPost(http, getResourceUrl(API_ENDPOINT_TENANTS, tenantName), updateObject); + return await httpPost({ + http, + url: getResourceUrl(API_ENDPOINT_TENANTS, tenantName), + body: updateObject, + }); } export async function updateTenancyConfiguration( @@ -116,7 +120,7 @@ export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { } export async function selectTenant(http: HttpStart, selectObject: TenantSelect): Promise { - return await httpPost(http, API_ENDPOINT_MULTITENANCY, selectObject); + return await httpPost({ http, url: API_ENDPOINT_MULTITENANCY, body: selectObject }); } export const RESOLVED_GLOBAL_TENANT = 'Global'; diff --git a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx index 518e19e6a..a1a9323c9 100644 --- a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx +++ b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx @@ -13,7 +13,13 @@ * permissions and limitations under the License. */ -import { transformUserData } from '../internal-user-list-utils'; +import { fetchUserNameList, getUserList, transformUserData } from '../internal-user-list-utils'; +import { httpGet } from '../request-utils'; +import * as InternalUserListUtils from '../internal-user-list-utils'; + +jest.mock('../../utils/request-utils', () => ({ + httpGet: jest.fn().mockResolvedValue({ data: {} }), +})); describe('Internal user list utils', () => { const userList = { @@ -32,4 +38,102 @@ describe('Internal user list utils', () => { ]; expect(result).toEqual(expectedUserList); }); + + it('getUserList calls httpGet with the correct parameters for internal users', async () => { + const httpMock = {}; // Mock HttpStart object + const userType = 'internalaccounts'; + const query = { dataSourceId: 'test' }; + + // Mock the response data from httpGet + const mockRawData = { + data: { + // your mocked data here + }, + }; + + // Mock the return value of getUserListRaw + jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); + + // Call the function you want to test + const test = await getUserList(httpMock, userType, query); + + // Assert that httpGet was called with the correct parameters + expect(httpGet).toHaveBeenCalledWith({ + http: httpMock, + url: '/api/v1/configuration/internalaccounts', + query, + }); + expect(test).toEqual([]); + }); + + it('getUserList calls httpGet with the correct parameters for service accounts', async () => { + const httpMock = {}; // Mock HttpStart object + const userType = 'serviceAccounts'; + const query = { dataSourceId: 'test' }; + + // Mock the response data from httpGet + const mockRawData = { + data: {}, + }; + + // Mock the return value of getUserListRaw + jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); + + // Call the function you want to test + const test = await getUserList(httpMock, userType, query); + + // Assert that httpGet was called with the correct parameters + expect(httpGet).toHaveBeenCalledWith({ + http: httpMock, + url: '/api/v1/configuration/serviceaccounts', + query, + }); + expect(test).toEqual([]); + }); + + it('fetchUserNameList calls httpGet with the correct parameters for service accounts', async () => { + const httpMock = {}; // Mock HttpStart object + const userType = 'serviceAccounts'; + + // Mock the response data from httpGet + const mockRawData = { + data: {}, + }; + + // Mock the return value of getUserListRaw + jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); + + // Call the function you want to test + const test = await fetchUserNameList(httpMock, userType); + + // Assert that httpGet was called with the correct parameters + expect(httpGet).toHaveBeenCalledWith({ + http: httpMock, + url: '/api/v1/configuration/serviceaccounts', + }); + expect(test).toEqual([]); + }); + + it('fetchUserNameList calls httpGet with the correct parameters for internal users', async () => { + const httpMock = {}; // Mock HttpStart object + const userType = 'internalaccounts'; + + // Mock the response data from httpGet + const mockRawData = { + data: {}, + }; + + // Mock the return value of getUserListRaw + jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); + + // Call the function you want to test + const test = await fetchUserNameList(httpMock, userType); + + // Assert that httpGet was called with the correct parameters + expect(httpGet).toHaveBeenCalledWith({ + http: httpMock, + url: '/api/v1/configuration/internalaccounts', + }); + expect(test).toEqual([]); + }); }); diff --git a/public/utils/datasource-utils.ts b/public/utils/datasource-utils.ts new file mode 100644 index 000000000..79de76a2f --- /dev/null +++ b/public/utils/datasource-utils.ts @@ -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. + */ + +import { DataSourceOption } from '../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; + +export function createDataSourceQuery(dataSourceId: string) { + return { dataSourceId }; +} + +export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: DataSourceOption) { + if (dataSourceEnabled) { + return `for ${cluster.label || 'Local cluster'}`; + } + return ''; +} diff --git a/public/utils/login-utils.tsx b/public/utils/login-utils.tsx index e66080c67..7c0d98bec 100644 --- a/public/utils/login-utils.tsx +++ b/public/utils/login-utils.tsx @@ -21,8 +21,12 @@ export async function validateCurrentPassword( userName: string, currentPassword: string ): Promise { - await httpPost(http, '/auth/login', { - username: userName, - password: currentPassword, + await httpPost({ + http, + url: '/auth/login', + body: { + username: userName, + password: currentPassword, + }, }); } diff --git a/public/utils/test/datasource-utils.test.ts b/public/utils/test/datasource-utils.test.ts new file mode 100644 index 000000000..a34ddd951 --- /dev/null +++ b/public/utils/test/datasource-utils.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { createDataSourceQuery, getClusterInfoIfEnabled } from '../datasource-utils'; + +describe('Tests datasource utils', () => { + 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'); + }); + + it('Tests the create DataSource query helper function', () => { + expect(createDataSourceQuery('test')).toStrictEqual({ dataSourceId: 'test' }); + }); +}); diff --git a/server/routes/index.ts b/server/routes/index.ts index 7668d358b..8d8da8c05 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -257,7 +257,12 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { if (request.params.resourceName === ResourceType.serviceAccounts.toLowerCase()) { esResp = await client.callAsCurrentUser('opensearch_security.listServiceAccounts'); } else if (request.params.resourceName === 'internalaccounts') { - esResp = await client.callAsCurrentUser('opensearch_security.listInternalAccounts'); + esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.listInternalAccounts' + ); } else { esResp = await wrapRouteWithDataSource( dataSourceEnabled, @@ -359,6 +364,9 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { resourceName: schema.string(), id: schema.string(), }), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async ( @@ -366,13 +374,17 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { request, response ): Promise> => { - const client = context.security_plugin.esClient.asScoped(request); - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.getResource', { - resourceName: request.params.resourceName, - id: request.params.id, - }); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.getResource', + { + resourceName: request.params.resourceName, + id: request.params.id, + } + ); return response.ok({ body: esResp[request.params.id] }); } catch (error) { return errorResponse(response, error); @@ -393,6 +405,9 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { minLength: 1, }), }), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async ( @@ -400,13 +415,17 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { request, response ): Promise> => { - const client = context.security_plugin.esClient.asScoped(request); - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.deleteResource', { - resourceName: request.params.resourceName, - id: request.params.id, - }); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.deleteResource', + { + resourceName: request.params.resourceName, + id: request.params.id, + } + ); return response.ok({ body: { message: esResp.message, @@ -436,6 +455,9 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { resourceName: schema.string(), }), body: schema.any(), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async ( @@ -448,13 +470,17 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { } catch (error) { return response.badRequest({ body: error }); } - const client = context.security_plugin.esClient.asScoped(request); - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.saveResourceWithoutId', { - resourceName: request.params.resourceName, - body: request.body, - }); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.saveResourceWithoutId', + { + resourceName: request.params.resourceName, + body: request.body, + } + ); return response.ok({ body: { message: esResp.message, @@ -480,6 +506,9 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { }), }), body: schema.any(), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async ( @@ -492,14 +521,18 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { } catch (error) { return response.badRequest({ body: error }); } - const client = context.security_plugin.esClient.asScoped(request); - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.saveResource', { - resourceName: request.params.resourceName, - id: request.params.id, - body: request.body, - }); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.saveResource', + { + resourceName: request.params.resourceName, + id: request.params.id, + body: request.body, + } + ); return response.ok({ body: { message: esResp.message, diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 3f21904d4..034b9f2cb 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -37,10 +37,25 @@ const createDataSource = () => { }; 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(); + cy.request( + 'GET', + `${Cypress.config( + 'baseUrl' + )}/api/saved_objects/_find?fields=id&fields=description&fields=title&per_page=10000&type=data-source` + ).then((resp) => { + if (resp && resp.body && resp.body.saved_objects) { + resp.body.saved_objects.map(({ id }) => { + cy.request({ + method: 'DELETE', + url: `${Cypress.config('baseUrl')}/api/saved_objects/data-source/${id}`, + body: { force: false }, + headers: { + 'osd-xsrf': true, + }, + }); + }); + } + }); }; describe('Multi-datasources enabled', () => { @@ -70,7 +85,7 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); }); - it('Checks Auth Tab', () => { + it.skip('Checks Auth Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); // Local cluster auth cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); @@ -78,5 +93,32 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); cy.contains('li.euiSelectableListItem', '9202').click(); cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); + // Data source persisted across tabs + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); + }); + + it('Checks Users Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); + // Create an internal user in the remote cluster + cy.contains('h3', 'Internal users'); + cy.contains('a', 'admin'); + // TODO replace these with navigating to urls that get read to determine datasource, since these are flaky + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('[data-test-subj="create-user"]').click(); + cy.get('[data-test-subj="name-text"]').focus().type('9202-user'); + cy.get('[data-test-subj="password"]').focus().type('myStrongPassword123!'); + cy.get('[data-test-subj="re-enter-password"]').focus().type('myStrongPassword123!'); + cy.get('[data-test-subj="submit-save-user"]').click(); + + // Internal user exists on the remote + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); + cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); + + // Internal user doesn't exist on local cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.contains('li.euiSelectableListItem', 'Local cluster').click(); + cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('not.exist'); }); }); diff --git a/test/helper/entity_operation.ts b/test/helper/entity_operation.ts index a35b94378..aabdfbd8a 100644 --- a/test/helper/entity_operation.ts +++ b/test/helper/entity_operation.ts @@ -33,3 +33,27 @@ export async function getEntityAsAdmin(root: Root, entityType: string, entityId: .get(root, `/api/v1/configuration/${entityType}/${entityId}`) .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); } + +export async function createOrUpdateEntityAsAdminWithDataSource( + root: Root, + entityType: string, + entityId: string, + body: any, + dataSourceId: string +) { + return await osdTestServer.request + .post(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send(body); +} + +export async function getEntityAsAdminWithDataSource( + root: Root, + entityType: string, + entityId: string, + dataSourceId: string +) { + return await osdTestServer.request + .get(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); +} diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index d584882f6..6e68a0574 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -26,7 +26,12 @@ import { AUTHORIZATION_HEADER_NAME, } from '../constant'; import { extractAuthCookie, getAuthCookie } from '../helper/cookie'; -import { createOrUpdateEntityAsAdmin, getEntityAsAdmin } from '../helper/entity_operation'; +import { + createOrUpdateEntityAsAdmin, + createOrUpdateEntityAsAdminWithDataSource, + getEntityAsAdmin, + getEntityAsAdminWithDataSource, +} from '../helper/entity_operation'; describe('start OpenSearch Dashboards server', () => { let root: Root; @@ -532,4 +537,77 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { // Getting auth info on an empty datasource calls local cluster expect(getAuthResponseRemoteDataSource.status).toEqual(200); }); + + it('create/get/update/list/delete internal user for external datasource', async () => { + const testUsername = `test_user_${Date.now()}`; + const testUserPassword = 'testUserPassword123'; + + const createUserResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + 'internalusers', + testUsername, + { + description: 'test user description', + password: testUserPassword, + backend_roles: ['arbitrary_backend_role'], + }, + dataSourceId + ); + expect(createUserResponse.status).toEqual(200); + + const getUserResponse = await getEntityAsAdminWithDataSource( + root, + 'internalusers', + testUsername, + dataSourceId + ); + expect(getUserResponse.status).toEqual(200); + expect(getUserResponse.body.description).toEqual('test user description'); + expect(getUserResponse.body.backend_roles).toContain('arbitrary_backend_role'); + + const listUserResponse = await osdTestServer.request + .get(root, `/api/v1/configuration/internalusers?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(listUserResponse.status).toEqual(200); + expect(listUserResponse.body.total).toBeGreaterThan(2); + expect(listUserResponse.body.data[testUsername]).toBeTruthy(); + + const updateUserResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + 'internalusers', + testUsername, + { + description: 'new description', + password: testUserPassword, + backend_roles: ['arbitrary_backend_role'], + }, + dataSourceId + ); + expect(updateUserResponse.status).toEqual(200); + + const getUpdatedUserResponse = await getEntityAsAdminWithDataSource( + root, + 'internalusers', + testUsername, + dataSourceId + ); + expect(getUpdatedUserResponse.status).toEqual(200); + expect(getUpdatedUserResponse.body.description).toEqual('new description'); + + const deleteUserResponse = await osdTestServer.request + .delete( + root, + `/api/v1/configuration/internalusers/${testUsername}?dataSourceId=${dataSourceId}` + ) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(deleteUserResponse.status).toEqual(200); + + const getDeletedUserResponse = await getEntityAsAdminWithDataSource( + root, + 'internalusers', + testUsername, + dataSourceId + ); + expect(getDeletedUserResponse.status).toEqual(404); + }); }); From 9bceddff37ac34f6c0b82a896f180cede6ebb354 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 28 Mar 2024 17:42:32 -0400 Subject: [PATCH 04/22] Adds SSL test support, compatibility test support (#1856) --------- Signed-off-by: Derek Ho --- .github/actions/download-plugin/action.yml | 29 ++----- .github/actions/run-cypress-tests/action.yml | 1 + ...ress-test-multidatasources-enabled-e2e.yml | 48 ++--------- .../cypress-test-tenancy-disabled.yml | 1 + .github/workflows/cypress-test.yml | 1 + .github/workflows/integration-test.yml | 81 ++++--------------- .../workflows/verify-binary-installation.yml | 1 + public/apps/configuration/top-nav-menu.tsx | 17 +++- .../multi_datasources_enabled.spec.js | 6 +- .../security_entity_api.test.ts | 4 +- 10 files changed, 52 insertions(+), 137 deletions(-) diff --git a/.github/actions/download-plugin/action.yml b/.github/actions/download-plugin/action.yml index c97e2e9c1..6be121f66 100644 --- a/.github/actions/download-plugin/action.yml +++ b/.github/actions/download-plugin/action.yml @@ -14,6 +14,10 @@ inputs: description: 'The version of security plugin that should be used, e.g "3.0.0.0"' required: true + download-location: + description: 'The location of where to download the plugin' + required: true + runs: using: "composite" steps: @@ -22,26 +26,5 @@ runs: -DremoteRepositories=https://aws.oss.sonatype.org/content/repositories/snapshots/ \ -Dartifact=org.opensearch.plugin:${{ inputs.plugin-name }}:${{ inputs.plugin-version }}-SNAPSHOT:zip \ -Dtransitive=false \ - -Ddest=${{ inputs.plugin-name }}.zip - shell: bash - - - name: Create Setup Script for Linux - if: ${{ runner.os == 'Linux' }} - run: | - cat > setup.sh <<'EOF' - chmod +x ./opensearch-${{ inputs.opensearch-version}}-SNAPSHOT/plugins/${{ inputs.plugin-name }}/tools/install_demo_configuration.sh - /bin/bash -c "yes | ./opensearch-${{ inputs.opensearch-version}}-SNAPSHOT/plugins/${{ inputs.plugin-name }}/tools/install_demo_configuration.sh -t" - echo "plugins.security.unsupported.restapi.allow_securityconfig_modification: true" >> ./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/config/opensearch.yml - echo "cluster.routing.allocation.disk.threshold_enabled: false" >> ./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/config/opensearch.yml - EOF - shell: bash - - - name: Create Setup Script for Windows - if: ${{ runner.os == 'Windows' }} - run: | - New-Item .\setup.bat -type file - Set-Content .\setup.bat -Value "powershell.exe -noexit -command `".\opensearch-${{ inputs.opensearch-version}}-SNAPSHOT\plugins\${{ inputs.plugin-name }}\tools\install_demo_configuration.bat -y -i -c -t`"" - Add-Content -Path .\setup.bat -Value "echo plugins.security.unsupported.restapi.allow_securityconfig_modification: true >> .\opensearch-${{ inputs.opensearch-version}}-SNAPSHOT\config\opensearch.yml" - Add-Content -Path .\setup.bat -Value "echo cluster.routing.allocation.disk.threshold_enabled: false >> .\opensearch-${{ inputs.opensearch-version}}-SNAPSHOT\config\opensearch.yml" - Get-Content .\setup.bat - shell: pwsh + -Ddest=${{ inputs.download-location }}.zip + shell: bash \ No newline at end of file diff --git a/.github/actions/run-cypress-tests/action.yml b/.github/actions/run-cypress-tests/action.yml index 6627243f0..678634455 100644 --- a/.github/actions/run-cypress-tests/action.yml +++ b/.github/actions/run-cypress-tests/action.yml @@ -34,6 +34,7 @@ runs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + download-location: ${{ env.PLUGIN_NAME }} - name: Run Opensearch with A Single Plugin uses: derek-ho/start-opensearch@v2 diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml index 71b01c69b..92540eb6d 100644 --- a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -31,45 +31,6 @@ jobs: 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 - # Add Custom Configuration to differentiate between local and remote cluster - name: Create Custom Configuration for Linux if: ${{ runner.os == 'Linux'}} @@ -120,22 +81,22 @@ jobs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + download-location: ${{env.PLUGIN_NAME}} - name: Run Opensearch with A Single Plugin - uses: derek-ho/start-opensearch@9202 + uses: derek-ho/start-opensearch@graceful-t 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: config_custom.yml - opensearch_yml_file: remote_opensearch.yml - opensearch_port: 9202 + port: 9202 - name: Check OpenSearch is running # Verify that the server is operational run: | - curl http://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! + curl https://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! -k shell: bash # Configure the Dashboard for multi datasources @@ -155,6 +116,7 @@ jobs: opensearch_security.cookie.secure: false data_source.enabled: true home.disableWelcomeScreen: true + data_source.ssl.verificationMode: none EOT - name: Run Cypress Tests diff --git a/.github/workflows/cypress-test-tenancy-disabled.yml b/.github/workflows/cypress-test-tenancy-disabled.yml index 8ba25a4c8..024dd9cbc 100644 --- a/.github/workflows/cypress-test-tenancy-disabled.yml +++ b/.github/workflows/cypress-test-tenancy-disabled.yml @@ -44,6 +44,7 @@ jobs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + download-location: ${{ env.PLUGIN_NAME }} - name: Run Opensearch with security uses: derek-ho/start-opensearch@v2 diff --git a/.github/workflows/cypress-test.yml b/.github/workflows/cypress-test.yml index ab5000383..c9a85ac6f 100644 --- a/.github/workflows/cypress-test.yml +++ b/.github/workflows/cypress-test.yml @@ -44,6 +44,7 @@ jobs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + download-location: ${{ env.PLUGIN_NAME }} - name: Run Opensearch with security uses: derek-ho/start-opensearch@v2 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8948f14b0..2a15958df 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -15,6 +15,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest , windows-latest ] + os_bwc_version: [2.4.0, 2.5.0, 2.6.0, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 2.11.0, 2.12.0, 3.0.0] runs-on: ${{ matrix.os }} steps: @@ -34,85 +35,35 @@ jobs: echo "PLUGIN_VERSION=$plugin_version" >> $GITHUB_ENV shell: bash - - uses: browser-actions/setup-geckodriver@v0.0.0 - - run: geckodriver --version - - - name: Set up Firefox browser - if: ${{ runner.os == 'Linux' }} - uses: browser-actions/setup-firefox@v1 - - - run: firefox --version - if: ${{ runner.os == 'Linux' }} - - # Browser-action version does not work on Windows - - name: Set up Firefox browser for Windows - if: ${{ runner.os == 'Windows' }} - uses: RyanL1997/setup-browser@main + - name: Download security plugin and create setup scripts for remote cluster + uses: ./.github/actions/download-plugin with: - browser: firefox - version: latest + opensearch-version: ${{ matrix.os_bwc_version }} + plugin-name: ${{ env.PLUGIN_NAME }} + download-location: ${{env.PLUGIN_NAME}}-${{matrix.os_bwc_version}} + plugin-version: ${{matrix.os_bwc_version}}.0 - - name: Download security plugin and create setup scripts + - name: Download security plugin and create setup scripts for local cluster uses: ./.github/actions/download-plugin with: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} + download-location: ${{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 + - name: Run Opensearch with A Single Plugin Remote Cluster + uses: derek-ho/start-opensearch@graceful-t with: - opensearch-version: ${{ env.OPENSEARCH_VERSION }} - plugins: "file:$(pwd)/opensearch-security.zip" + opensearch-version: ${{ matrix.os_bwc_version }} + plugins: "file:$(pwd)/opensearch-security-${{matrix.os_bwc_version}}.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 + port: 9202 - - name: Check OpenSearch is running - # Verify that the server is operational + - name: Check OpenSearch remote is running run: | - curl http://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! + curl https://localhost:9202/_cat/plugins -v -u admin:${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} -k shell: bash - name: Run Opensearch with security diff --git a/.github/workflows/verify-binary-installation.yml b/.github/workflows/verify-binary-installation.yml index 7c89029d8..c19bc3c81 100644 --- a/.github/workflows/verify-binary-installation.yml +++ b/.github/workflows/verify-binary-installation.yml @@ -36,6 +36,7 @@ jobs: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} plugin-version: ${{ env.PLUGIN_VERSION }} + download-location: ${{ env.PLUGIN_NAME }} - name: Run Opensearch with security uses: derek-ho/start-opensearch@v2 diff --git a/public/apps/configuration/top-nav-menu.tsx b/public/apps/configuration/top-nav-menu.tsx index ce2dfb1a0..c582700e5 100644 --- a/public/apps/configuration/top-nav-menu.tsx +++ b/public/apps/configuration/top-nav-menu.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { DataSourceSelectableConfig } from 'src/plugins/data_source_management/public'; import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; -import { PLUGIN_NAME } from '../../../common'; import { AppDependencies } from '../types'; export interface TopNavMenuProps extends AppDependencies { @@ -25,6 +24,21 @@ export interface TopNavMenuProps extends AppDependencies { selectedDataSource: DataSourceOption; } +const compatibleVersion = new Set([ + '2.1', + '2.2', + '2.3', + '2.4', + '2.5', + '2.6', + '2.7', + '2.8', + '2.9', + '2.10', + '2.11', + '2.12', +]); + export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { const { coreStart, @@ -48,6 +62,7 @@ export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { savedObjects: coreStart.savedObjects.client, notifications: coreStart.notifications, activeOption: [selectedDataSource], + dataSourceFilter: (ds) => compatibleVersion.has(ds.attributes.version), onSelectedDataSources: (dataSources) => { // single select for now setDataSource(dataSources[0]); diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 034b9f2cb..93a810243 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -23,7 +23,7 @@ const createDataSource = () => { body: { attributes: { title: `9202`, - endpoint: `http://localhost:9202`, + endpoint: `https://localhost:9202`, auth: { type: 'username_password', credentials: { @@ -85,7 +85,7 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); }); - it.skip('Checks Auth Tab', () => { + it('Checks Auth Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); // Local cluster auth cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); @@ -98,7 +98,7 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); }); - it('Checks Users Tab', () => { + it.skip('Checks Users Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); // Create an internal user in the remote cluster cy.contains('h3', 'Internal users'); diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index 6e68a0574..c3ca21986 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -438,7 +438,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { plugins: { scanDirs: [resolve(__dirname, '../..')], }, - data_source: { enabled: true }, + data_source: { enabled: true, ssl: { verificationMode: 'none' } }, opensearch: { hosts: ['https://localhost:9200'], ignoreVersionMismatch: true, @@ -468,7 +468,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { attributes: { title: 'test', description: '', - endpoint: 'http://localhost:9202', + endpoint: 'https://localhost:9202', auth: { type: 'username_password', credentials: { From be3e939ea907e33b6ea7e7a0108e5611a1a91258 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:30:45 -0400 Subject: [PATCH 05/22] [Multiple datasource] Adds datasource picker to Permissions tab (#1857) * Adds datasource picker to Permissions tab Signed-off-by: Darshit Chanpura * Finalizes changes for Permissions tab dataSource picker Signed-off-by: Darshit Chanpura * Fixes Unit tests Signed-off-by: Darshit Chanpura * Adds integration tests Signed-off-by: Darshit Chanpura * Adds cypress test for Permission tab with remote data source Signed-off-by: Darshit Chanpura * Completes cypress test for Permissions tab, fixes cypress test for Users tab and fixes a spelling error Signed-off-by: Darshit Chanpura * Updates unit tests and reverts a change in cypress test Signed-off-by: Darshit Chanpura * Addresses nits to make tests more robust Signed-off-by: Darshit Chanpura * Fixes unit tests Signed-off-by: Darshit Chanpura --------- Signed-off-by: Darshit Chanpura --- .../permission-list/permission-list.tsx | 54 +++++++-- .../test/permission-list.test.tsx | 34 ++++-- .../__snapshots__/role-list.test.tsx.snap | 1 + .../utils/action-groups-utils.tsx | 34 ++++-- .../configuration/utils/display-utils.tsx | 9 +- .../__snapshots__/display-utils.test.tsx.snap | 2 + .../multi_datasources_enabled.spec.js | 84 +++++++++++-- test/helper/entity_operation.ts | 21 ++++ .../security_entity_api.test.ts | 114 +++++++++++++++--- 9 files changed, 296 insertions(+), 57 deletions(-) diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index 96f0c425b..ec90dd7d5 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -34,7 +34,14 @@ import { Query, } from '@elastic/eui'; import { difference } from 'lodash'; -import React, { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react'; +import React, { + Dispatch, + ReactNode, + SetStateAction, + useCallback, + useState, + useContext, +} from 'react'; import { AppDependencies } from '../../../types'; import { Action, DataObject, ActionGroupItem, ExpandedRowMapInterface } from '../../types'; import { @@ -54,6 +61,9 @@ import { useDeleteConfirmState } from '../../utils/delete-confirm-modal-utils'; import { useContextMenuState } from '../../utils/context-menu'; import { generateResourceName } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { DataSourceContext } from '../../app-router'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; export function renderBooleanToCheckMark(value: boolean): React.ReactNode { return value ? : ''; @@ -77,7 +87,7 @@ export function toggleRowDetails( }); } -export function renderRowExpanstionArrow( +export function renderRowExpansionArrow( itemIdToExpandedRowMap: ExpandedRowMapInterface, actionGroupDict: DataObject, setItemIdToExpandedRowMap: Dispatch> @@ -129,7 +139,7 @@ function getColumns( align: RIGHT_ALIGNMENT, width: '40px', isExpander: true, - render: renderRowExpanstionArrow( + render: renderRowExpansionArrow( itemIdToExpandedRowMap, actionGroupDict, setItemIdToExpandedRowMap @@ -182,6 +192,8 @@ export function PermissionList(props: AppDependencies) { const [selection, setSelection] = React.useState([]); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; + // Modal state const [editModal, setEditModal] = useState(null); @@ -194,7 +206,10 @@ export function PermissionList(props: AppDependencies) { const fetchData = useCallback(async () => { try { setLoading(true); - const actionGroups = await fetchActionGroups(props.coreStart.http); + const actionGroups = await fetchActionGroups( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setActionGroupDict(actionGroups); setPermissionList(await mergeAllPermissions(actionGroups)); } catch (e) { @@ -203,16 +218,20 @@ export function PermissionList(props: AppDependencies) { } finally { setLoading(false); } - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); React.useEffect(() => { fetchData(); - }, [props.coreStart.http, fetchData]); + }, [props.coreStart.http, fetchData, dataSource.id]); const handleDelete = async () => { const groupsToDelete: string[] = selection.map((r) => r.name); try { - await requestDeleteActionGroups(props.coreStart.http, groupsToDelete); + await requestDeleteActionGroups( + props.coreStart.http, + groupsToDelete, + createDataSourceQuery(dataSource.id) + ); setPermissionList(difference(permissionList, selection)); setSelection([]); } catch (e) { @@ -276,9 +295,12 @@ export function PermissionList(props: AppDependencies) { handleClose={() => setEditModal(null)} handleSave={async (groupName, allowedAction) => { try { - await updateActionGroup(props.coreStart.http, groupName, { - allowed_actions: allowedAction, - }); + await updateActionGroup( + props.coreStart.http, + groupName, + { allowed_actions: allowedAction }, + createDataSourceQuery(dataSource.id) + ); setEditModal(null); fetchData(); addToast({ @@ -301,7 +323,11 @@ export function PermissionList(props: AppDependencies) { }; const createActionGroupMenuItems = [ - showEditModal('', Action.create, [])}> + showEditModal('', Action.create, [])} + > Create from blank , +

Permissions

diff --git a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx index 965ccea01..856a3e8fb 100644 --- a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx +++ b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx @@ -19,7 +19,7 @@ import { PermissionList, renderBooleanToCheckMark, toggleRowDetails, - renderRowExpanstionArrow, + renderRowExpansionArrow, } from '../permission-list'; import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui'; import { @@ -49,6 +49,11 @@ jest.mock('../../../utils/toast-utils', () => ({ useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + describe('Permission list page ', () => { const sampleActionGroup: PermissionListingItem = { name: 'group', @@ -73,7 +78,7 @@ describe('Permission list page ', () => { describe('renderRowExpanstionArrow', () => { it('should render down arrow when collapsed', () => { - const renderFunc = renderRowExpanstionArrow({}, {}, jest.fn()); + const renderFunc = renderRowExpansionArrow({}, {}, jest.fn()); const Wrapper = () => <>{renderFunc(sampleActionGroup)}; const component = shallow(); @@ -81,7 +86,7 @@ describe('Permission list page ', () => { }); it('should render up arrow when expanded', () => { - const renderFunc = renderRowExpanstionArrow( + const renderFunc = renderRowExpansionArrow( { [sampleActionGroup.name]: sampleActionGroup }, {}, jest.fn() @@ -94,6 +99,12 @@ describe('Permission list page ', () => { }); describe('PermissionList', () => { + const mockCoreStart = { + http: 1, + }; + const dataSourceQuery = { + dataSourceId: 'test', + }; it('render empty', () => { const component = shallow( { jest.spyOn(React, 'useEffect').mockImplementationOnce((f) => f()); shallow( ); - expect(fetchActionGroups).toBeCalled(); + expect(fetchActionGroups).toBeCalledWith(mockCoreStart.http, dataSourceQuery); }); it('fetch data error', () => { @@ -147,7 +158,7 @@ describe('Permission list page ', () => { it('submit change', () => { const component = shallow( { const submitFunc = component.find(PermissionEditModal).prop('handleSave'); submitFunc('group1', []); - expect(updateActionGroup).toBeCalled(); + expect(updateActionGroup).toBeCalledWith( + mockCoreStart.http, + 'group1', + { allowed_actions: [] }, + dataSourceQuery + ); }); it('submit change error', () => { @@ -186,7 +202,7 @@ describe('Permission list page ', () => { it('delete action group', (done) => { shallow( { deleteFunc(); process.nextTick(() => { - expect(requestDeleteActionGroups).toBeCalled(); + expect(requestDeleteActionGroups).toBeCalledWith(mockCoreStart.http, [], dataSourceQuery); done(); }); }); diff --git a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap index 9826ce5c9..a9cbacb15 100644 --- a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap @@ -17,6 +17,7 @@ exports[`Role list Render columns render Customization column 1`] = ` > Reserved diff --git a/public/apps/configuration/utils/action-groups-utils.tsx b/public/apps/configuration/utils/action-groups-utils.tsx index 048448ead..7852f86ba 100644 --- a/public/apps/configuration/utils/action-groups-utils.tsx +++ b/public/apps/configuration/utils/action-groups-utils.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart, HttpFetchQuery } from 'opensearch-dashboards/public'; import { map } from 'lodash'; import { API_ENDPOINT_ACTIONGROUPS, CLUSTER_PERMISSIONS, INDEX_PERMISSIONS } from '../constants'; import { DataObject, ActionGroupItem, ActionGroupUpdate, ObjectsMessage } from '../types'; @@ -29,10 +29,14 @@ export interface PermissionListingItem { hasIndexPermission: boolean; } -export async function fetchActionGroups(http: HttpStart): Promise> { +export async function fetchActionGroups( + http: HttpStart, + query: HttpFetchQuery +): Promise> { const actiongroups = await httpGet>({ http, url: API_ENDPOINT_ACTIONGROUPS, + query, }); return actiongroups.data; } @@ -50,8 +54,11 @@ export function transformActionGroupsToListingFormat( })); } -export async function fetchActionGroupListing(http: HttpStart): Promise { - return transformActionGroupsToListingFormat(await fetchActionGroups(http)); +export async function fetchActionGroupListing( + http: HttpStart, + query: HttpFetchQuery +): Promise { + return transformActionGroupsToListingFormat(await fetchActionGroups(http, query)); } function getClusterSinglePermissions(): PermissionListingItem[] { @@ -76,8 +83,11 @@ function getIndexSinglePermissions(): PermissionListingItem[] { })); } -export async function getAllPermissionsListing(http: HttpStart): Promise { - return mergeAllPermissions(await fetchActionGroups(http)); +export async function getAllPermissionsListing( + http: HttpStart, + query: HttpFetchQuery +): Promise { + return mergeAllPermissions(await fetchActionGroups(http, query)); } export async function mergeAllPermissions( @@ -91,17 +101,23 @@ export async function mergeAllPermissions( export async function updateActionGroup( http: HttpStart, groupName: string, - updateObject: ActionGroupUpdate + updateObject: ActionGroupUpdate, + query: HttpFetchQuery ): Promise { return await httpPost({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), body: updateObject, + query, }); } -export async function requestDeleteActionGroups(http: HttpStart, groups: string[]) { +export async function requestDeleteActionGroups( + http: HttpStart, + groups: string[], + query: HttpFetchQuery +) { for (const group of groups) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group) }); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group), query }); } } diff --git a/public/apps/configuration/utils/display-utils.tsx b/public/apps/configuration/utils/display-utils.tsx index d4b33fb62..f11584ab4 100644 --- a/public/apps/configuration/utils/display-utils.tsx +++ b/public/apps/configuration/utils/display-utils.tsx @@ -70,7 +70,14 @@ export function renderCustomization(reserved: boolean, props: UIProps) { - {reserved ? 'Reserved' : 'Custom'} + + {reserved ? 'Reserved' : 'Custom'} +
); diff --git a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap index f81deee72..f1795f2a5 100644 --- a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap +++ b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap @@ -16,6 +16,7 @@ exports[`Display utils Render Customization column when reserved = False 1`] = ` > Custom @@ -39,6 +40,7 @@ exports[`Display utils Render Customization column when reserved = True 1`] = ` > Reserved diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 93a810243..8026c3da7 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -22,8 +22,8 @@ const createDataSource = () => { }, body: { attributes: { - title: `9202`, - endpoint: `https://localhost:9202`, + title: '9202', + endpoint: 'https://localhost:9202', auth: { type: 'username_password', credentials: { @@ -36,6 +36,15 @@ const createDataSource = () => { }); }; +const closeToast = () => { + // remove browser incompatibiltiy toast causing flakyness (cause it has higher z-index than Create button making it invisible) + cy.get('body').then((body) => { + if (body.find('[data-test-subj="toastCloseButton"]').length > 0) { + cy.get('[data-test-subj="toastCloseButton"]').click(); + } + }); +}; + const deleteAllDataSources = () => { cy.request( 'GET', @@ -77,7 +86,7 @@ describe('Multi-datasources enabled', () => { 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('[title="9202"]').click(); cy.get('[data-test-subj="purge-cache"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); @@ -91,21 +100,23 @@ describe('Multi-datasources enabled', () => { cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); // Remote cluster auth cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', '9202').click(); - cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); - // Data source persisted across tabs - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); + cy.get('[title="9202"]').click(); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); }); - it.skip('Checks Users Tab', () => { + it('Checks Users Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); // Create an internal user in the remote cluster cy.contains('h3', 'Internal users'); cy.contains('a', 'admin'); - // TODO replace these with navigating to urls that get read to determine datasource, since these are flaky + + closeToast(); + + // select remote data source cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('[title="9202"]').click(); + + // create a user on remote data source cy.get('[data-test-subj="create-user"]').click(); cy.get('[data-test-subj="name-text"]').focus().type('9202-user'); cy.get('[data-test-subj="password"]').focus().type('myStrongPassword123!'); @@ -113,12 +124,59 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="submit-save-user"]').click(); // Internal user exists on the remote - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); // Internal user doesn't exist on local cluster cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', 'Local cluster').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('not.exist'); }); + + it('Checks Permissions Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/permissions'); + // Create a permission in the remote cluster + cy.contains('h3', 'Permissions'); + + closeToast(); + + // Select remote cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="9202"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + + // Create an action group + cy.get('[id="Create action group"]').click(); + cy.get('[id="create-from-blank"]').click(); + cy.get('[data-test-subj="name-text"]') + .focus() + .type('test_permission_ag', { force: true }) + .should('have.value', 'test_permission_ag'); + cy.get('[data-test-subj="comboBoxInput"]').focus().type('some_permission'); + cy.get('[id="submit"]').click(); + + // Permission exists on the remote data source + cy.get('[data-text="Customization"]').click(); + cy.get('[data-test-subj="filter-custom-action-groups"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('exist'); + + // Permission doesn't exist on local cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('not.exist'); + }); }); diff --git a/test/helper/entity_operation.ts b/test/helper/entity_operation.ts index aabdfbd8a..aad5da56f 100644 --- a/test/helper/entity_operation.ts +++ b/test/helper/entity_operation.ts @@ -57,3 +57,24 @@ export async function getEntityAsAdminWithDataSource( .get(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); } + +export async function getAllEntitiesAsAdminWithDataSource( + root: Root, + entityType: string, + dataSourceId: string +) { + return await osdTestServer.request + .get(root, `/api/v1/configuration/${entityType}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); +} + +export async function deleteEntityAsAdminWithDataSource( + root: Root, + entityType: string, + entityId: string, + dataSourceId: string +) { + return await osdTestServer.request + .delete(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); +} diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index c3ca21986..4f745c9de 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -29,6 +29,8 @@ import { extractAuthCookie, getAuthCookie } from '../helper/cookie'; import { createOrUpdateEntityAsAdmin, createOrUpdateEntityAsAdminWithDataSource, + deleteEntityAsAdminWithDataSource, + getAllEntitiesAsAdminWithDataSource, getEntityAsAdmin, getEntityAsAdminWithDataSource, } from '../helper/entity_operation'; @@ -478,6 +480,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { }, }, }); + expect(createDataSource.status).toEqual(200); dataSourceId = createDataSource.body.id; }); @@ -541,10 +544,11 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { it('create/get/update/list/delete internal user for external datasource', async () => { const testUsername = `test_user_${Date.now()}`; const testUserPassword = 'testUserPassword123'; + const entityType = 'internalusers'; const createUserResponse = await createOrUpdateEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, { description: 'test user description', @@ -557,7 +561,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { const getUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); @@ -565,16 +569,18 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { expect(getUserResponse.body.description).toEqual('test user description'); expect(getUserResponse.body.backend_roles).toContain('arbitrary_backend_role'); - const listUserResponse = await osdTestServer.request - .get(root, `/api/v1/configuration/internalusers?dataSourceId=${dataSourceId}`) - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + const listUserResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); expect(listUserResponse.status).toEqual(200); expect(listUserResponse.body.total).toBeGreaterThan(2); expect(listUserResponse.body.data[testUsername]).toBeTruthy(); const updateUserResponse = await createOrUpdateEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, { description: 'new description', @@ -587,27 +593,107 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { const getUpdatedUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); expect(getUpdatedUserResponse.status).toEqual(200); expect(getUpdatedUserResponse.body.description).toEqual('new description'); - const deleteUserResponse = await osdTestServer.request - .delete( - root, - `/api/v1/configuration/internalusers/${testUsername}?dataSourceId=${dataSourceId}` - ) - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + const deleteUserResponse = await deleteEntityAsAdminWithDataSource( + root, + entityType, + testUsername, + dataSourceId + ); expect(deleteUserResponse.status).toEqual(200); const getDeletedUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); expect(getDeletedUserResponse.status).toEqual(404); }); + + it('CRUD Permissions for external datasource', async () => { + const entityType = 'actiongroups'; + const testActionGroupName = `test_action_group_${Date.now()}`; + + const createActionGroupResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + { + allowed_actions: ['some_allowed_action'], + }, + dataSourceId + ); + expect(createActionGroupResponse.status).toEqual(200); + + const getActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getActionGroupsResponse.status).toEqual(200); + expect(getActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe(true); + expect(getActionGroupsResponse.body.data[testActionGroupName].allowed_actions).toContain( + 'some_allowed_action' + ); + + // verify that this AG is not created in Local Cluster + const getActionGroupsResponseLocalCluster = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + '' + ); + expect(getActionGroupsResponseLocalCluster.status).toEqual(200); + expect(getActionGroupsResponseLocalCluster.body.data?.hasOwnProperty(testActionGroupName)).toBe( + false + ); + + const updatePermissionResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + { + allowed_actions: ['some_allowed_action', 'another_permission'], + }, + dataSourceId + ); + expect(updatePermissionResponse.status).toEqual(200); + + const getUpdatedActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getUpdatedActionGroupsResponse.status).toEqual(200); + expect(getUpdatedActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe( + true + ); + expect(getUpdatedActionGroupsResponse.body.data[testActionGroupName].allowed_actions).toContain( + 'another_permission' + ); + + const deleteActionGroupResponse = await deleteEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + dataSourceId + ); + expect(deleteActionGroupResponse.status).toEqual(200); + + const getDeletedActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getDeletedActionGroupsResponse.status).toEqual(200); + expect(getDeletedActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe( + false + ); + }); }); From 0a0af00b03f54bb176870a6619a6c0a3006c28e9 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 2 Apr 2024 14:59:33 -0400 Subject: [PATCH 06/22] Add multi datasource support for the tenant and audit log tabs (#1861) * Add multi datasource support for the tenanct and audit log tabs Signed-off-by: Derek Ho * Add integraiton tests for audit log with and without multi datasource Signed-off-by: Derek Ho * Add cypress test support for tenancy and audit log Signed-off-by: Derek Ho * Add toast support and assert existance of page element to make test unflaky Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- public/apps/configuration/app-router.tsx | 8 +- .../audit-logging-edit-settings.tsx | 46 ++++++++++-- .../panels/audit-logging/audit-logging.tsx | 33 +++++++-- .../__snapshots__/audit-logging.test.tsx.snap | 19 ++++- .../test/audit-logging-edit-settings.test.tsx | 16 ++-- .../audit-logging/test/audit-logging.test.tsx | 4 + .../panels/tenant-list/tenant-list.tsx | 10 ++- .../__snapshots__/app-router.test.tsx.snap | 1 - .../utils/audit-logging-utils.tsx | 17 +++-- server/routes/index.ts | 33 ++++++--- .../multi_datasources_enabled.spec.js | 42 +++++++++++ test/jest_integration/constants.ts | 74 +++++++++++++++++++ .../security_entity_api.test.ts | 45 +++++++++++ 13 files changed, 305 insertions(+), 43 deletions(-) create mode 100644 test/jest_integration/constants.ts diff --git a/public/apps/configuration/app-router.tsx b/public/apps/configuration/app-router.tsx index ea9ae73c0..491623ab3 100644 --- a/public/apps/configuration/app-router.tsx +++ b/public/apps/configuration/app-router.tsx @@ -149,15 +149,13 @@ export interface DataSourceContextType { setDataSource: React.Dispatch>; } +export const LocalCluster = { label: 'Local cluster', id: '' }; + export const DataSourceContext = createContext(null); export function AppRouter(props: AppDependencies) { const setGlobalBreadcrumbs = flow(getBreadcrumbs, props.coreStart.chrome.setBreadcrumbs); - const [dataSource, setDataSource] = useState({ - id: '', - label: 'Local cluster', - checked: 'on', - }); + const [dataSource, setDataSource] = useState(LocalCluster); return ( diff --git a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx index de5d0086f..a0224be79 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { EuiButton, EuiFlexGroup, @@ -35,15 +35,20 @@ import { ResourceType } from '../../../../../common'; import { getAuditLogging, updateAuditLogging } from '../../utils/audit-logging-utils'; import { useToastState } from '../../utils/toast-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { DataSourceContext } from '../../app-router'; +import { createDataSourceQuery, getClusterInfoIfEnabled } from '../../../../utils/datasource-utils'; interface AuditLoggingEditSettingProps extends AppDependencies { setting: 'general' | 'compliance'; } export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const [editConfig, setEditConfig] = React.useState({}); const [toasts, addToast, removeToast] = useToastState(); const [invalidSettings, setInvalidSettings] = React.useState([]); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const handleChange = (path: string, val: boolean | string[] | SettingMapItem) => { setEditConfig((previousEditedConfig) => { @@ -63,7 +68,10 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { React.useEffect(() => { const fetchConfig = async () => { try { - const fetchedConfig = await getAuditLogging(props.coreStart.http); + const fetchedConfig = await getAuditLogging( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setEditConfig(fetchedConfig); } catch (e) { console.log(e); @@ -71,7 +79,7 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { }; fetchConfig(); - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); const renderSaveAndCancel = () => { return ( @@ -106,7 +114,11 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { const saveConfig = async (configToUpdate: AuditLoggingSettings) => { try { - await updateAuditLogging(props.coreStart.http, configToUpdate); + await updateAuditLogging( + props.coreStart.http, + configToUpdate, + createDataSourceQuery(dataSource.id) + ); const addSuccessToast = (text: string) => { const successToast: Toast = { @@ -121,9 +133,13 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { }; if (props.setting === 'general') { - addSuccessToast('General settings saved'); + addSuccessToast( + `General settings saved ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` + ); } else { - addSuccessToast('Compliance settings saved'); + addSuccessToast( + `Compliance settings saved ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` + ); } window.location.href = buildHashUrl(ResourceType.auditLogging); @@ -132,7 +148,11 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { id: 'update-result', color: 'danger', iconType: 'alert', - title: 'Failed to update audit configuration due to ' + e?.message, + title: + `Failed to update audit configuration ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )} due to ` + e?.message, }; addToast(failureToast); @@ -237,5 +257,15 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { content = renderComplianceSetting(); } - return
{content}
; + return ( +
+ + {content} +
+ ); } diff --git a/public/apps/configuration/panels/audit-logging/audit-logging.tsx b/public/apps/configuration/panels/audit-logging/audit-logging.tsx index 5ca10c84c..73b5af782 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging.tsx @@ -28,7 +28,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React from 'react'; +import React, { useContext } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { AppDependencies } from '../../../types'; import { ResourceType } from '../../../../../common'; @@ -43,6 +43,9 @@ import { import { AuditLoggingSettings } from './types'; import { ViewSettingGroup } from './view-setting-group'; import { DocLinks } from '../../constants'; +import { DataSourceContext } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface AuditLoggingProps extends AppDependencies { fromType: string; @@ -134,13 +137,18 @@ export function renderComplianceSettings(config: AuditLoggingSettings) { export function AuditLogging(props: AuditLoggingProps) { const [configuration, setConfiguration] = React.useState({}); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const onSwitchChange = async () => { try { const updatedConfiguration = { ...configuration }; updatedConfiguration.enabled = !updatedConfiguration.enabled; - await updateAuditLogging(props.coreStart.http, updatedConfiguration); + await updateAuditLogging( + props.coreStart.http, + updatedConfiguration, + createDataSourceQuery(dataSource.id) + ); setConfiguration(updatedConfiguration); } catch (e) { @@ -151,7 +159,10 @@ export function AuditLogging(props: AuditLoggingProps) { React.useEffect(() => { const fetchData = async () => { try { - const auditLogging = await getAuditLogging(props.coreStart.http); + const auditLogging = await getAuditLogging( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setConfiguration(auditLogging); } catch (e) { // TODO: switch to better error handling. @@ -160,7 +171,7 @@ export function AuditLogging(props: AuditLoggingProps) { }; fetchData(); - }, [props.coreStart.http, props.fromType]); + }, [props.coreStart.http, props.fromType, dataSource.id]); const statusPanel = renderStatusPanel(onSwitchChange, configuration.enabled || false); @@ -174,7 +185,7 @@ export function AuditLogging(props: AuditLoggingProps) { {statusPanel} - + @@ -226,5 +237,15 @@ export function AuditLogging(props: AuditLoggingProps) { ); } - return
{content}
; + return ( +
+ + {content} +
+ ); } diff --git a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap index 6d8e9167f..3e64fa6c9 100644 --- a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap +++ b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap @@ -241,6 +241,21 @@ exports[`Audit logs render when AuditLoggingSettings.enabled is true 1`] = `
+

@@ -321,7 +336,9 @@ exports[`Audit logs render when AuditLoggingSettings.enabled is true 1`] = ` - + diff --git a/public/apps/configuration/panels/audit-logging/test/audit-logging-edit-settings.test.tsx b/public/apps/configuration/panels/audit-logging/test/audit-logging-edit-settings.test.tsx index a7b52d428..ead990e33 100644 --- a/public/apps/configuration/panels/audit-logging/test/audit-logging-edit-settings.test.tsx +++ b/public/apps/configuration/panels/audit-logging/test/audit-logging-edit-settings.test.tsx @@ -21,6 +21,10 @@ import { buildHashUrl } from '../../../utils/url-builder'; import { ResourceType } from '../../../../../../common'; jest.mock('../../../utils/audit-logging-utils'); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); // eslint-disable-next-line const mockAuditLoggingUtils = require('../../../utils/audit-logging-utils'); @@ -53,7 +57,7 @@ describe('Audit logs edit', () => { shallow( { shallow( { shallow( { const component = shallow( { const component = shallow( { const component = shallow( ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); // eslint-disable-next-line const mockAuditLoggingUtils = require('../../../utils/audit-logging-utils'); diff --git a/public/apps/configuration/panels/tenant-list/tenant-list.tsx b/public/apps/configuration/panels/tenant-list/tenant-list.tsx index f1d3079b5..3fd5b89da 100644 --- a/public/apps/configuration/panels/tenant-list/tenant-list.tsx +++ b/public/apps/configuration/panels/tenant-list/tenant-list.tsx @@ -23,7 +23,7 @@ import { EuiButton, } from '@elastic/eui'; import { Route } from 'react-router-dom'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useContext } from 'react'; import { ManageTab } from './manage_tab'; import { ConfigureTab1 } from './configure_tab1'; import { AppDependencies } from '../../../types'; @@ -32,6 +32,8 @@ import { displayBoolean } from '../../utils/display-utils'; import { DocLinks } from '../../constants'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; import { TenantInstructionView } from './tenant-instruction-view'; +import { DataSourceContext, LocalCluster } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; interface TenantListProps extends AppDependencies { tabID: string; @@ -134,6 +136,12 @@ export function TenantList(props: TenantListProps) { return ( <> + {}} + selectedDataSource={LocalCluster} + />

Multi-tenancy

diff --git a/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap index 68026a1c1..4696ecc12 100644 --- a/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap +++ b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap @@ -5,7 +5,6 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab value={ Object { "dataSource": Object { - "checked": "on", "id": "", "label": "Local cluster", }, diff --git a/public/apps/configuration/utils/audit-logging-utils.tsx b/public/apps/configuration/utils/audit-logging-utils.tsx index 72b04771d..517babf95 100644 --- a/public/apps/configuration/utils/audit-logging-utils.tsx +++ b/public/apps/configuration/utils/audit-logging-utils.tsx @@ -13,16 +13,23 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; import { AuditLoggingSettings } from '../panels/audit-logging/types'; import { API_ENDPOINT_AUDITLOGGING, API_ENDPOINT_AUDITLOGGING_UPDATE } from '../constants'; import { httpGet, httpPost } from './request-utils'; -export async function updateAuditLogging(http: HttpStart, updateObject: AuditLoggingSettings) { - return await httpPost({ http, url: API_ENDPOINT_AUDITLOGGING_UPDATE, body: updateObject }); +export async function updateAuditLogging( + http: HttpStart, + updateObject: AuditLoggingSettings, + query: HttpFetchQuery +) { + return await httpPost({ http, url: API_ENDPOINT_AUDITLOGGING_UPDATE, body: updateObject, query }); } -export async function getAuditLogging(http: HttpStart): Promise { - const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_AUDITLOGGING }); +export async function getAuditLogging( + http: HttpStart, + query: HttpFetchQuery +): Promise { + const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_AUDITLOGGING, query }); return rawConfiguration?.config; } diff --git a/server/routes/index.ts b/server/routes/index.ts index 8d8da8c05..38d7566bb 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -674,18 +674,24 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { router.get( { path: `${API_PREFIX}/configuration/audit`, - validate: false, + validate: { + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), + }, }, async ( context, request, response ): Promise> => { - const client = context.security_plugin.esClient.asScoped(request); - - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.getAudit'); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.getAudit' + ); return response.ok({ body: esResp, @@ -759,15 +765,22 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { path: `${API_PREFIX}/configuration/audit/config`, validate: { body: schema.any(), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), }, }, async (context, request, response) => { - const client = context.security_plugin.esClient.asScoped(request); - let esResp; try { - esResp = await client.callAsCurrentUser('opensearch_security.saveAudit', { - body: request.body, - }); + const esResp = await wrapRouteWithDataSource( + dataSourceEnabled, + context, + request, + 'opensearch_security.saveAudit', + { + body: request.body, + } + ); return response.ok({ body: { message: esResp.message, diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 8026c3da7..486a70aa3 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -179,4 +179,46 @@ describe('Multi-datasources enabled', () => { ); cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('not.exist'); }); + + it('Checks Tenancy Tab', () => { + // Datasource is locked to local cluster for tenancy tab + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/tenants'); + cy.contains('h1', 'Multi-tenancy'); + cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('be.disabled'); + }); + + it('Checks Audit Logs Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auditLogging'); + cy.get('[data-test-subj="general-settings"]').should('exist'); + + // Select remote cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="9202"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + + cy.get('[data-test-subj="general-settings-configure"]').click(); + cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('contain', '9202'); + + cy.get('[data-test-subj="comboBoxInput"]').last().type('blah'); + cy.get('[data-test-subj="save"]').click(); + + cy.get('[data-test-subj="general-settings"]').should('contain', 'blah'); + + // Select local cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + + cy.get('[data-test-subj="general-settings"]').should('not.contain', 'blah'); + }); }); diff --git a/test/jest_integration/constants.ts b/test/jest_integration/constants.ts new file mode 100644 index 000000000..30516f730 --- /dev/null +++ b/test/jest_integration/constants.ts @@ -0,0 +1,74 @@ +/* + * 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. + */ + +export const testAuditLogDisabledSettings = { + enabled: false, + audit: { + enable_rest: false, + disabled_rest_categories: ['FAILED_LOGIN', 'AUTHENTICATED'], + enable_transport: true, + disabled_transport_categories: ['GRANTED_PRIVILEGES'], + resolve_bulk_requests: true, + log_request_body: false, + resolve_indices: true, + exclude_sensitive_headers: true, + ignore_users: ['admin'], + ignore_requests: ['SearchRequest', 'indices:data/read/*'], + }, + compliance: { + enabled: true, + internal_config: false, + external_config: false, + read_metadata_only: false, + read_watched_fields: { + indexName1: ['field1', 'fields-*'], + }, + read_ignore_users: ['opensearchdashboardsserver', 'operator/*'], + write_metadata_only: false, + write_log_diffs: false, + write_watched_indices: ['indexName2', 'indexPatterns-*'], + write_ignore_users: ['admin'], + }, +}; + +export const testAuditLogEnabledSettings = { + enabled: true, + audit: { + enable_rest: false, + disabled_rest_categories: ['FAILED_LOGIN', 'AUTHENTICATED'], + enable_transport: true, + disabled_transport_categories: ['GRANTED_PRIVILEGES'], + resolve_bulk_requests: true, + log_request_body: false, + resolve_indices: true, + exclude_sensitive_headers: true, + ignore_users: ['admin'], + ignore_requests: ['SearchRequest', 'indices:data/read/*'], + }, + compliance: { + enabled: true, + internal_config: false, + external_config: false, + read_metadata_only: false, + read_watched_fields: { + indexName1: ['field1', 'fields-*'], + }, + read_ignore_users: ['opensearchdashboardsserver', 'operator/*'], + write_metadata_only: false, + write_log_diffs: false, + write_watched_indices: ['indexName2', 'indexPatterns-*'], + write_ignore_users: ['admin'], + }, +}; diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index 4f745c9de..0546259fe 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -34,6 +34,11 @@ import { getEntityAsAdmin, getEntityAsAdminWithDataSource, } from '../helper/entity_operation'; +import { + testAuditLogDisabledSettings, + testAuditLogEnabledSettings, + testAuditLogSettings, +} from './constants'; describe('start OpenSearch Dashboards server', () => { let root: Root; @@ -428,6 +433,19 @@ describe('start OpenSearch Dashboards server', () => { }); expect(response.status).toEqual(200); }); + + it('Audit logging', async () => { + const getAuditLoggingResponse = await osdTestServer.request + .get(root, '/api/v1/configuration/audit') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(getAuditLoggingResponse.status).toEqual(200); + + const changeAuditLogResponse = await osdTestServer.request + .post(root, '/api/v1/configuration/audit/config') + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send(testAuditLogEnabledSettings); + expect(changeAuditLogResponse.status).toEqual(200); + }); }); describe('start OpenSearch Dashboards server multi datasources enabled', () => { @@ -696,4 +714,31 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { false ); }); + + // Skip tenant multi datasource tests since it is locked to local cluster + + it('Audit logging', async () => { + const getAuditLoggingResponseRemote = await osdTestServer.request + .get(root, `/api/v1/configuration/audit?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(getAuditLoggingResponseRemote.status).toEqual(200); + + const changeAuditLogResponseRemote = await osdTestServer.request + .post(root, `/api/v1/configuration/audit/config?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS) + .send(testAuditLogDisabledSettings); + expect(changeAuditLogResponseRemote.status).toEqual(200); + + const getAuditLoggingResponse = await osdTestServer.request + .get(root, `/api/v1/configuration/audit`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(getAuditLoggingResponse.status).toEqual(200); + expect(getAuditLoggingResponse.body.config.enabled).toEqual(true); + + const checkAuditLogSettingsRemote = await osdTestServer.request + .get(root, `/api/v1/configuration/audit?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + expect(checkAuditLogSettingsRemote.status).toEqual(200); + expect(checkAuditLogSettingsRemote.body.config.enabled).toEqual(false); + }); }); From d8d1c8571299e6f3dcd15f23b63e345c9f3b88d8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:26:01 -0400 Subject: [PATCH 07/22] Adds datasource picker for Roles tab and its sub-tabs (#1871) * Adds datasource picker for Roles tab and its sub-tabs Signed-off-by: Darshit Chanpura * Fixes jest:ui task Signed-off-by: Darshit Chanpura * Adds integration test for roles datasource picker Signed-off-by: Darshit Chanpura * Adds cypress test for roles tab Signed-off-by: Darshit Chanpura * fixFix ResizeObserver loop error Signed-off-by: Darshit Chanpura * Makes query param required Signed-off-by: Darshit Chanpura --------- Signed-off-by: Darshit Chanpura --- public/apps/account/utils.tsx | 2 +- .../panels/role-edit/role-edit.tsx | 56 ++++-- .../test/role-edit-filtering.test.tsx | 10 +- .../panels/role-edit/test/role-edit.test.tsx | 19 +- .../apps/configuration/panels/role-list.tsx | 36 +++- .../role-mapping/role-edit-mapped-user.tsx | 34 +++- .../test/role-edit-mapped-user.test.tsx | 19 +- .../panels/role-view/role-view.tsx | 44 ++++- .../__snapshots__/role-view.test.tsx.snap | 68 +++++++ .../panels/role-view/test/role-view.test.tsx | 4 + .../__snapshots__/role-list.test.tsx.snap | 2 +- .../panels/test/role-list.test.tsx | 5 + .../configuration/utils/display-utils.tsx | 4 +- .../utils/internal-user-list-utils.tsx | 8 +- .../apps/configuration/utils/request-utils.ts | 61 ++++--- .../configuration/utils/role-detail-utils.tsx | 22 ++- .../configuration/utils/role-list-utils.tsx | 21 ++- .../utils/role-mapping-utils.tsx | 17 +- .../apps/configuration/utils/tenant-utils.tsx | 16 +- .../__snapshots__/display-utils.test.tsx.snap | 4 +- public/utils/dashboards-info-utils.tsx | 6 +- .../multi_datasources_enabled.spec.js | 51 +++++- .../security_entity_api.test.ts | 168 ++++++++++++++++++ 23 files changed, 569 insertions(+), 108 deletions(-) diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index df3c480be..e411895bc 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -30,7 +30,7 @@ export function fetchAccountInfo(http: HttpStart): Promise { } export async function fetchAccountInfoSafe(http: HttpStart): Promise { - return httpGetWithIgnores(http, API_ENDPOINT_ACCOUNT_INFO, [401]); + return httpGetWithIgnores({ http, url: API_ENDPOINT_ACCOUNT_INFO, ignores: [401] }); } export async function logout(http: HttpStart, logoutUrl?: string): Promise { diff --git a/public/apps/configuration/panels/role-edit/role-edit.tsx b/public/apps/configuration/panels/role-edit/role-edit.tsx index a3364fe0b..ead2ee93d 100644 --- a/public/apps/configuration/panels/role-edit/role-edit.tsx +++ b/public/apps/configuration/panels/role-edit/role-edit.tsx @@ -24,7 +24,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { isEmpty } from 'lodash'; import { BreadcrumbsPageDependencies } from '../../../types'; import { CLUSTER_PERMISSIONS, DocLinks, INDEX_PERMISSIONS } from '../../constants'; @@ -57,6 +57,9 @@ import { setCrossPageToast } from '../../utils/storage-utils'; import { ExternalLink } from '../../utils/display-utils'; import { generateResourceName } from '../../utils/resource-utils'; import { NameRow } from '../../utils/name-row'; +import { DataSourceContext } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface RoleEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -86,12 +89,18 @@ export function RoleEdit(props: RoleEditDeps) { const [isFormValid, setIsFormValid] = useState(true); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; + React.useEffect(() => { const action = props.action; if (action === 'edit' || action === 'duplicate') { const fetchData = async () => { try { - const roleData = await getRoleDetail(props.coreStart.http, props.sourceRoleName); + const roleData = await getRoleDetail( + props.coreStart.http, + props.sourceRoleName, + createDataSourceQuery(dataSource.id) + ); setRoleClusterPermission(roleData.cluster_permissions.map(stringToComboBoxOption)); setRoleIndexPermission(buildIndexPermissionState(roleData.index_permissions)); setRoleTenantPermission(buildTenantPermissionState(roleData.tenant_permissions)); @@ -105,13 +114,16 @@ export function RoleEdit(props: RoleEditDeps) { fetchData(); } - }, [addToast, props.action, props.coreStart.http, props.sourceRoleName]); + }, [addToast, props.action, props.coreStart.http, props.sourceRoleName, dataSource.id]); const [actionGroups, setActionGroups] = useState>([]); React.useEffect(() => { const fetchActionGroupNames = async () => { try { - const actionGroupsObject = await fetchActionGroups(props.coreStart.http); + const actionGroupsObject = await fetchActionGroups( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setActionGroups(Object.entries(actionGroupsObject)); } catch (e) { addToast(createUnknownErrorToast('actionGroup', 'load data')); @@ -120,13 +132,15 @@ export function RoleEdit(props: RoleEditDeps) { }; fetchActionGroupNames(); - }, [addToast, props.coreStart.http]); + }, [addToast, props.coreStart.http, dataSource.id]); const [tenantNames, setTenantNames] = React.useState([]); React.useEffect(() => { const fetchTenantNames = async () => { try { - setTenantNames(await fetchTenantNameList(props.coreStart.http)); + setTenantNames( + await fetchTenantNameList(props.coreStart.http, createDataSourceQuery(dataSource.id)) + ); } catch (e) { addToast(createUnknownErrorToast('tenant', 'load data')); console.error(e); @@ -134,7 +148,7 @@ export function RoleEdit(props: RoleEditDeps) { }; fetchTenantNames(); - }, [addToast, props.coreStart.http]); + }, [addToast, props.coreStart.http, dataSource.id]); const updateRoleHandler = async () => { try { @@ -146,11 +160,16 @@ export function RoleEdit(props: RoleEditDeps) { (v: RoleTenantPermissionStateClass) => !isEmpty(v.tenantPatterns) ); - await updateRole(props.coreStart.http, roleName, { - cluster_permissions: roleClusterPermission.map(comboBoxOptionToString), - index_permissions: unbuildIndexPermissionState(validIndexPermission), - tenant_permissions: unbuildTenantPermissionState(validTenantPermission), - }); + await updateRole( + props.coreStart.http, + roleName, + { + cluster_permissions: roleClusterPermission.map(comboBoxOptionToString), + index_permissions: unbuildIndexPermissionState(validIndexPermission), + tenant_permissions: unbuildTenantPermissionState(validTenantPermission), + }, + createDataSourceQuery(dataSource.id) + ); setCrossPageToast(buildUrl(ResourceType.roles, Action.view, roleName), { id: 'updateRoleSucceeded', @@ -215,6 +234,12 @@ export function RoleEdit(props: RoleEditDeps) { return ( <> + {props.buildBreadcrumbs(TITLE_TEXT_DICT[props.action])} @@ -271,7 +296,12 @@ export function RoleEdit(props: RoleEditDeps) {
- + {props.action === 'edit' ? 'Update' : 'Create'} diff --git a/public/apps/configuration/panels/role-edit/test/role-edit-filtering.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit-filtering.test.tsx index 68a521ab6..36d67fdc9 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit-filtering.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit-filtering.test.tsx @@ -33,6 +33,7 @@ jest.mock('../../../utils/role-detail-utils', () => ({ }), updateRole: jest.fn(), })); + jest.mock('../../../utils/action-groups-utils'); jest.mock('../cluster-permission-panel', () => ({ @@ -43,6 +44,11 @@ jest.mock('../index-permission-panel', () => ({ IndexPermissionPanel: jest.fn(() => null) as jest.Mock, })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + describe('Role edit filtering', () => { const sampleSourceRole = 'role'; const mockCoreStart = { @@ -94,7 +100,7 @@ describe('Role edit filtering', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -150,7 +156,7 @@ describe('Role edit filtering', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx index 01c16bc1b..68bfd602b 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx @@ -38,6 +38,10 @@ jest.mock('../../../utils/action-groups-utils', () => ({ })); jest.mock('../../../utils/tenant-utils'); jest.mock('../../../utils/storage-utils'); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); describe('Role edit', () => { const sampleSourceRole = 'role'; @@ -116,11 +120,16 @@ describe('Role edit', () => { // click update component.find(EuiButton).last().simulate('click'); - expect(updateRole).toBeCalledWith(mockCoreStart.http, '', { - cluster_permissions: [], - index_permissions: [], - tenant_permissions: [], - }); + expect(updateRole).toBeCalledWith( + mockCoreStart.http, + '', + { + cluster_permissions: [], + index_permissions: [], + tenant_permissions: [], + }, + { dataSourceId: 'test' } + ); process.nextTick(() => { expect(setCrossPageToast).toHaveBeenCalled(); diff --git a/public/apps/configuration/panels/role-list.tsx b/public/apps/configuration/panels/role-list.tsx index c9ecab34b..0603c715d 100644 --- a/public/apps/configuration/panels/role-list.tsx +++ b/public/apps/configuration/panels/role-list.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { EuiFlexGroup, EuiText, @@ -54,6 +54,9 @@ import { showTableStatusMessage } from '../utils/loading-spinner-utils'; import { useDeleteConfirmState } from '../utils/delete-confirm-modal-utils'; import { useContextMenuState } from '../utils/context-menu'; import { DocLinks } from '../constants'; +import { DataSourceContext } from '../app-router'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; +import { createDataSourceQuery } from '../../../utils/datasource-utils'; const columns: Array> = [ { @@ -105,13 +108,20 @@ export function RoleList(props: AppDependencies) { const [errorFlag, setErrorFlag] = React.useState(false); const [selection, setSelection] = React.useState([]); const [loading, setLoading] = useState(false); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { const fetchData = async () => { try { setLoading(true); - const rawRoleData = await fetchRole(props.coreStart.http); - const rawRoleMappingData = await fetchRoleMapping(props.coreStart.http); + const rawRoleData = await fetchRole( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); + const rawRoleMappingData = await fetchRoleMapping( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); const processedData = transformRoleData(rawRoleData, rawRoleMappingData); setRoleData(processedData); } catch (e) { @@ -123,12 +133,16 @@ export function RoleList(props: AppDependencies) { }; fetchData(); - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); const handleDelete = async () => { const rolesToDelete: string[] = selection.map((r) => r.roleName); try { - await requestDeleteRoles(props.coreStart.http, rolesToDelete); + await requestDeleteRoles( + props.coreStart.http, + rolesToDelete, + createDataSourceQuery(dataSource.id) + ); // Refresh from server (calling fetchData) does not work here, the server still return the roles // that had been just deleted, probably because ES takes some time to sync to all nodes. // So here remove the selected roles from local memory directly. @@ -250,6 +264,12 @@ export function RoleList(props: AppDependencies) { return ( <> +

Roles

@@ -278,7 +298,11 @@ export function RoleList(props: AppDependencies) { {actionsMenu} - + Create role diff --git a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx index 44909bef5..c9f9d24b6 100644 --- a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx +++ b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx @@ -23,7 +23,7 @@ import { EuiTitle, EuiGlobalToastList, } from '@elastic/eui'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { BreadcrumbsPageDependencies } from '../../../types'; import { InternalUsersPanel } from './users-panel'; import { @@ -43,6 +43,9 @@ import { createErrorToast, createUnknownErrorToast, useToastState } from '../../ import { DocLinks } from '../../constants'; import { setCrossPageToast } from '../../utils/storage-utils'; import { ExternalLink } from '../../utils/display-utils'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { DataSourceContext } from '../../app-router'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface RoleEditMappedUserProps extends BreadcrumbsPageDependencies { roleName: string; @@ -60,13 +63,15 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { const [userNames, setUserNames] = useState([]); const [hosts, setHosts] = React.useState([]); const [toasts, addToast, removeToast] = useToastState(); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { const fetchData = async () => { try { const originalRoleMapData: RoleMappingDetail | undefined = await getRoleMappingData( props.coreStart.http, - props.roleName + props.roleName, + createDataSourceQuery(dataSource.id) ); if (originalRoleMapData) { setInternalUsers(originalRoleMapData.users.map(stringToComboBoxOption)); @@ -80,12 +85,18 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { }; fetchData(); - }, [addToast, props.coreStart.http, props.roleName]); + }, [addToast, props.coreStart.http, props.roleName, dataSource.id]); React.useEffect(() => { const fetchInternalUserNames = async () => { try { - setUserNames(await fetchUserNameList(props.coreStart.http)); + setUserNames( + await fetchUserNameList( + props.coreStart.http, + ResourceType.users, + createDataSourceQuery(dataSource.id) + ) + ); } catch (e) { addToast(createUnknownErrorToast('fetchInternalUserNames', 'load data')); console.error(e); @@ -93,7 +104,7 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { }; fetchInternalUserNames(); - }, [addToast, props.coreStart.http]); + }, [addToast, props.coreStart.http, dataSource.id]); const internalUserOptions = userNames.map(stringToComboBoxOption); const updateRoleMappingHandler = async () => { @@ -108,7 +119,12 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { hosts, }; - await updateRoleMapping(props.coreStart.http, props.roleName, updateObject); + await updateRoleMapping( + props.coreStart.http, + props.roleName, + updateObject, + createDataSourceQuery(dataSource.id) + ); setCrossPageToast( buildUrl(ResourceType.roles, Action.view, props.roleName, SubAction.mapuser), { @@ -135,6 +151,12 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { return ( <> + {props.buildBreadcrumbs(props.roleName, TITLE_TEXT_DICT[SubAction.mapuser])} diff --git a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx index 3cf1b3c73..314ca803f 100644 --- a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx +++ b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx @@ -26,6 +26,10 @@ jest.mock('../../../utils/role-mapping-utils'); jest.mock('../../../utils/internal-user-list-utils', () => ({ fetchUserNameList: jest.fn().mockReturnValue([]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); // eslint-disable-next-line const roleMappingUtils = require('../../../utils/role-mapping-utils'); @@ -106,11 +110,16 @@ describe('Role mapping edit', () => { // click update component.find('#map').last().simulate('click'); - expect(updateRoleMapping).toBeCalledWith(mockCoreStart.http, sampleRole, { - users: [], - backend_roles: [], - hosts: [], - }); + expect(updateRoleMapping).toBeCalledWith( + mockCoreStart.http, + sampleRole, + { + users: [], + backend_roles: [], + hosts: [], + }, + { dataSourceId: 'test' } + ); }); it('submit update error', () => { diff --git a/public/apps/configuration/panels/role-view/role-view.tsx b/public/apps/configuration/panels/role-view/role-view.tsx index 723d546da..5f1ba3e5f 100644 --- a/public/apps/configuration/panels/role-view/role-view.tsx +++ b/public/apps/configuration/panels/role-view/role-view.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { EuiButton, @@ -70,6 +70,9 @@ import { showTableStatusMessage } from '../../utils/loading-spinner-utils'; import { useContextMenuState } from '../../utils/context-menu'; import { requestDeleteRoles } from '../../utils/role-list-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; +import { DataSourceContext } from '../../app-router'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface RoleViewProps extends BreadcrumbsPageDependencies { roleName: string; @@ -108,6 +111,7 @@ export function RoleView(props: RoleViewProps) { const [toasts, addToast, removeToast] = useToastState(); const [isReserved, setIsReserved] = React.useState(false); const [loading, setLoading] = React.useState(false); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const PERMISSIONS_TAB_INDEX = 0; const MAP_USER_TAB_INDEX = 1; @@ -116,15 +120,26 @@ export function RoleView(props: RoleViewProps) { const fetchData = async () => { try { setLoading(true); - const originalRoleMapData = await getRoleMappingData(props.coreStart.http, props.roleName); + const originalRoleMapData = await getRoleMappingData( + props.coreStart.http, + props.roleName, + createDataSourceQuery(dataSource.id) + ); if (originalRoleMapData) { setMappedUsers(transformRoleMappingData(originalRoleMapData)); setHosts(originalRoleMapData.hosts); } - const actionGroups = await fetchActionGroups(props.coreStart.http); + const actionGroups = await fetchActionGroups( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setActionGroupDict(actionGroups); - const roleData = await getRoleDetail(props.coreStart.http, props.roleName); + const roleData = await getRoleDetail( + props.coreStart.http, + props.roleName, + createDataSourceQuery(dataSource.id) + ); setIsReserved(roleData.reserved); setRoleClusterPermission(roleData.cluster_permissions); setRoleIndexPermission(transformRoleIndexPermissions(roleData.index_permissions)); @@ -139,7 +154,7 @@ export function RoleView(props: RoleViewProps) { }; fetchData(); - }, [addToast, props.coreStart.http, props.roleName, props.prevAction]); + }, [addToast, props.coreStart.http, props.roleName, props.prevAction, dataSource.id]); const handleRoleMappingDelete = async () => { try { @@ -155,7 +170,12 @@ export function RoleView(props: RoleViewProps) { backend_roles: difference(externalIdentities, usersToDelete), hosts, }; - await updateRoleMapping(props.coreStart.http, props.roleName, updateObject); + await updateRoleMapping( + props.coreStart.http, + props.roleName, + updateObject, + createDataSourceQuery(dataSource.id) + ); setMappedUsers(difference(mappedUsers, selection)); setSelection([]); @@ -350,7 +370,11 @@ export function RoleView(props: RoleViewProps) { color="danger" onClick={async () => { try { - await requestDeleteRoles(props.coreStart.http, [props.roleName]); + await requestDeleteRoles( + props.coreStart.http, + [props.roleName], + createDataSourceQuery(dataSource.id) + ); setCrossPageToast(buildUrl(ResourceType.roles), { id: 'deleteRole', color: 'success', @@ -384,6 +408,12 @@ export function RoleView(props: RoleViewProps) { return ( <> + {props.buildBreadcrumbs(props.roleName)} diff --git a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap index 6b65e56f7..576e409b2 100644 --- a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap +++ b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap @@ -2,6 +2,40 @@ exports[`Role view basic rendering when permission tab is selected 1`] = ` + + ({ createUnknownErrorToast: jest.fn(), useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); describe('Role view', () => { const setState = jest.fn(); diff --git a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap index a9cbacb15..346ab0454 100644 --- a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap @@ -17,7 +17,7 @@ exports[`Role list Render columns render Customization column 1`] = ` > Reserved diff --git a/public/apps/configuration/panels/test/role-list.test.tsx b/public/apps/configuration/panels/test/role-list.test.tsx index a70dac666..becbfe806 100644 --- a/public/apps/configuration/panels/test/role-list.test.tsx +++ b/public/apps/configuration/panels/test/role-list.test.tsx @@ -36,6 +36,11 @@ jest.mock('../../utils/context-menu', () => ({ jest.mock('../../utils/delete-confirm-modal-utils', () => ({ useDeleteConfirmState: jest.fn().mockReturnValue([jest.fn(), '']), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + // eslint-disable-next-line const mockRoleListUtils = require('../../utils/role-list-utils'); diff --git a/public/apps/configuration/utils/display-utils.tsx b/public/apps/configuration/utils/display-utils.tsx index f11584ab4..236bcf555 100644 --- a/public/apps/configuration/utils/display-utils.tsx +++ b/public/apps/configuration/utils/display-utils.tsx @@ -72,9 +72,7 @@ export function renderCustomization(reserved: boolean, props: UIProps) { {reserved ? 'Reserved' : 'Custom'} diff --git a/public/apps/configuration/utils/internal-user-list-utils.tsx b/public/apps/configuration/utils/internal-user-list-utils.tsx index 5f8ed3470..3f3e73e1c 100644 --- a/public/apps/configuration/utils/internal-user-list-utils.tsx +++ b/public/apps/configuration/utils/internal-user-list-utils.tsx @@ -65,6 +65,10 @@ export async function getUserList( return transformUserData(rawData.data); } -export async function fetchUserNameList(http: HttpStart, userType: string): Promise { - return Object.keys((await getUserListRaw(http, userType)).data); +export async function fetchUserNameList( + http: HttpStart, + userType: string, + query?: HttpFetchQuery +): Promise { + return Object.keys((await getUserListRaw(http, userType, query)).data); } diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index b3cb2110b..676c1e0f9 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -22,12 +22,15 @@ interface RequestType { query?: HttpFetchQuery; } -export async function request( - requestFunc: HttpHandler, - url: string, - body?: object, - query?: HttpFetchQuery -): Promise { +interface RequestParams { + requestFunc: HttpHandler; + url: string; + body?: object; + query?: HttpFetchQuery; +} + +export async function request(params: RequestParams): Promise { + const { requestFunc, url, body, query } = params; if (body) { return (await requestFunc(url, { body: JSON.stringify(body), query })) as T; } @@ -36,21 +39,28 @@ export async function request( export async function httpGet(params: RequestType): Promise { const { http, url, body, query } = params; - return await request(http.get, url, body, query); + return await request({ requestFunc: http.get, url, body, query }); } export async function httpPost(params: RequestType): Promise { const { http, url, body, query } = params; - return await request(http.post, url, body, query); + return await request({ requestFunc: http.post, url, body, query }); } export async function httpPut(http: HttpStart, url: string, body?: object): Promise { - return await request(http.put, url, body); + return await request({ requestFunc: http.put, url, body }); } export async function httpDelete(params: RequestType): Promise { const { http, url, body, query } = params; - return await request(http.delete, url, body, query); + return await request({ requestFunc: http.delete, url, body, query }); +} + +interface RequestTypeWithIgnore extends RequestType { + ignores: number[]; +} +interface RequestParamsWithIgnore extends RequestParams { + ignores: number[]; } /** @@ -60,13 +70,11 @@ export async function httpDelete(params: RequestType): Promise { * @param ignores the error codes to be ignored */ export async function requestWithIgnores( - requestFunc: HttpHandler, - url: string, - ignores: number[], - body?: object + params: RequestParamsWithIgnore ): Promise { + const { requestFunc, url, ignores, body, query } = params; try { - return await request(requestFunc, url, body); + return await request({ requestFunc, url, body, query }); } catch (e) { if (!ignores.includes(e?.response?.status)) { throw e; @@ -74,26 +82,21 @@ export async function requestWithIgnores( } } -export async function httpGetWithIgnores( - http: HttpStart, - url: string, - ignores: number[] -): Promise { - return await requestWithIgnores(http.get, url, ignores); +export async function httpGetWithIgnores(params: RequestTypeWithIgnore): Promise { + const { http, url, ignores, query } = params; + return await requestWithIgnores({ requestFunc: http.get, url, ignores, query }); } export async function httpPostWithIgnores( - http: HttpStart, - url: string, - ignores: number[] + params: RequestTypeWithIgnore ): Promise { - return await requestWithIgnores(http.post, url, ignores); + const { http, url, ignores, query } = params; + return await requestWithIgnores({ requestFunc: http.post, url, ignores, query }); } export async function httpDeleteWithIgnores( - http: HttpStart, - url: string, - ignores: number[] + params: RequestTypeWithIgnore ): Promise { - return await requestWithIgnores(http.delete, url, ignores); + const { http, url, ignores, query } = params; + return await requestWithIgnores({ requestFunc: http.delete, url, ignores, query }); } diff --git a/public/apps/configuration/utils/role-detail-utils.tsx b/public/apps/configuration/utils/role-detail-utils.tsx index 3686ece7d..6bd011742 100644 --- a/public/apps/configuration/utils/role-detail-utils.tsx +++ b/public/apps/configuration/utils/role-detail-utils.tsx @@ -13,20 +13,34 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_ROLES } from '../constants'; import { RoleDetail, RoleUpdate } from '../types'; import { httpGet, httpPost } from './request-utils'; import { getResourceUrl } from './resource-utils'; -export async function getRoleDetail(http: HttpStart, roleName: string): Promise { - return await httpGet({ http, url: getResourceUrl(API_ENDPOINT_ROLES, roleName) }); +export async function getRoleDetail( + http: HttpStart, + roleName: string, + query: HttpFetchQuery +): Promise { + return await httpGet({ + http, + url: getResourceUrl(API_ENDPOINT_ROLES, roleName), + query, + }); } -export async function updateRole(http: HttpStart, roleName: string, updateObject: RoleUpdate) { +export async function updateRole( + http: HttpStart, + roleName: string, + updateObject: RoleUpdate, + query: HttpFetchQuery +) { return await httpPost({ http, url: getResourceUrl(API_ENDPOINT_ROLES, roleName), body: updateObject, + query, }); } diff --git a/public/apps/configuration/utils/role-list-utils.tsx b/public/apps/configuration/utils/role-list-utils.tsx index 7eefd9c69..5092085dc 100644 --- a/public/apps/configuration/utils/role-list-utils.tsx +++ b/public/apps/configuration/utils/role-list-utils.tsx @@ -14,7 +14,7 @@ */ import { map, chain } from 'lodash'; -import { HttpStart } from '../../../../../../src/core/public'; +import { HttpStart, HttpFetchQuery } from '../../../../../../src/core/public'; import { API_ENDPOINT_ROLES, API_ENDPOINT_ROLESMAPPING } from '../constants'; import { httpDelete, httpDeleteWithIgnores, httpGet } from './request-utils'; import { getResourceUrl } from './resource-utils'; @@ -92,19 +92,24 @@ export function buildSearchFilterOptions(roleList: any[], attrName: string): Arr } // Submit request to delete given roles. No error handling in this function. -export async function requestDeleteRoles(http: HttpStart, roles: string[]) { +export async function requestDeleteRoles(http: HttpStart, roles: string[], query: HttpFetchQuery) { for (const role of roles) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ROLES, role) }); - await httpDeleteWithIgnores(http, getResourceUrl(API_ENDPOINT_ROLESMAPPING, role), [404]); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ROLES, role), query }); + await httpDeleteWithIgnores({ + http, + url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, role), + ignores: [404], + query, + }); } } // TODO: have a type definition for it -export function fetchRole(http: HttpStart): Promise { - return httpGet({ http, url: API_ENDPOINT_ROLES }); +export function fetchRole(http: HttpStart, query: HttpFetchQuery): Promise { + return httpGet({ http, url: API_ENDPOINT_ROLES, query }); } // TODO: have a type definition for it -export function fetchRoleMapping(http: HttpStart): Promise { - return httpGet({ http, url: API_ENDPOINT_ROLESMAPPING }); +export function fetchRoleMapping(http: HttpStart, query: HttpFetchQuery): Promise { + return httpGet({ http, url: API_ENDPOINT_ROLESMAPPING, query }); } diff --git a/public/apps/configuration/utils/role-mapping-utils.tsx b/public/apps/configuration/utils/role-mapping-utils.tsx index 426539fab..856f7c685 100644 --- a/public/apps/configuration/utils/role-mapping-utils.tsx +++ b/public/apps/configuration/utils/role-mapping-utils.tsx @@ -14,7 +14,7 @@ */ import { map } from 'lodash'; -import { HttpStart } from '../../../../../../src/core/public'; +import { HttpFetchQuery, HttpStart } from '../../../../../../src/core/public'; import { API_ENDPOINT_ROLESMAPPING } from '../constants'; import { RoleMappingDetail } from '../types'; import { httpGetWithIgnores, httpPost } from './request-utils'; @@ -30,12 +30,13 @@ export enum UserType { external = 'Backend role', } -export async function getRoleMappingData(http: HttpStart, roleName: string) { - return httpGetWithIgnores( +export async function getRoleMappingData(http: HttpStart, roleName: string, query: HttpFetchQuery) { + return httpGetWithIgnores({ http, - getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), - [404] - ); + url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), + ignores: [404], + query, + }); } export function transformRoleMappingData(rawData: RoleMappingDetail): MappedUsersListing[] { @@ -55,11 +56,13 @@ export function transformRoleMappingData(rawData: RoleMappingDetail): MappedUser export async function updateRoleMapping( http: HttpStart, roleName: string, - updateObject: RoleMappingDetail + updateObject: RoleMappingDetail, + query: HttpFetchQuery ) { return await httpPost({ http, url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), body: updateObject, + query, }); } diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index 2aa1309dc..329acbd44 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; import { map } from 'lodash'; import React from 'react'; import { i18n } from '@osd/i18n'; @@ -63,12 +63,18 @@ export const PRIVATE_USER_DICT: { [key: string]: string } = { Description: 'Only visible to the current logged in user', }; -export async function fetchTenants(http: HttpStart): Promise> { - return (await httpGet>({ http, url: API_ENDPOINT_TENANTS })).data; +export async function fetchTenants( + http: HttpStart, + query: HttpFetchQuery +): Promise> { + return (await httpGet>({ http, url: API_ENDPOINT_TENANTS, query })).data; } -export async function fetchTenantNameList(http: HttpStart): Promise { - return Object.keys(await fetchTenants(http)); +export async function fetchTenantNameList( + http: HttpStart, + query: HttpFetchQuery +): Promise { + return Object.keys(await fetchTenants(http, query)); } export function transformTenantData(rawTenantData: DataObject): Tenant[] { diff --git a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap index f1795f2a5..11f384b0f 100644 --- a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap +++ b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap @@ -16,7 +16,7 @@ exports[`Display utils Render Customization column when reserved = False 1`] = ` > Custom @@ -40,7 +40,7 @@ exports[`Display utils Render Customization column when reserved = True 1`] = ` > Reserved diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index d54897122..9c636ee47 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -23,5 +23,9 @@ export async function getDashboardsInfo(http: HttpStart) { } export async function getDashboardsInfoSafe(http: HttpStart): Promise { - return httpGetWithIgnores(http, API_ENDPOINT_DASHBOARDSINFO, [401]); + return httpGetWithIgnores({ + http, + url: API_ENDPOINT_DASHBOARDSINFO, + ignores: [401], + }); } diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 486a70aa3..229fa448d 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -69,6 +69,7 @@ const deleteAllDataSources = () => { describe('Multi-datasources enabled', () => { before(() => { + deleteAllDataSources(); localStorage.setItem('opendistro::security::tenant::saved', '""'); localStorage.setItem('home:newThemeModal:show', 'false'); createDataSource(); @@ -164,10 +165,11 @@ describe('Multi-datasources enabled', () => { .should('have.value', 'test_permission_ag'); cy.get('[data-test-subj="comboBoxInput"]').focus().type('some_permission'); cy.get('[id="submit"]').click(); + closeToast(); // Permission exists on the remote data source cy.get('[data-text="Customization"]').click(); - cy.get('[data-test-subj="filter-custom-action-groups"]').click(); + cy.get('[data-test-subj="filter-custom"]').click(); cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('exist'); // Permission doesn't exist on local cluster @@ -206,6 +208,7 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="general-settings-configure"]').click(); cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('contain', '9202'); + closeToast(); cy.get('[data-test-subj="comboBoxInput"]').last().type('blah'); cy.get('[data-test-subj="save"]').click(); @@ -221,4 +224,50 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="general-settings"]').should('not.contain', 'blah'); }); + + it('Checks Roles Tab', () => { + Cypress.on('uncaught:exception', (err) => !err.message.includes('ResizeObserver')); + + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/roles'); + cy.contains('h3', 'Roles'); + + closeToast(); + + // select remote data source + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="9202"]').click(); + + // create a role on remote data source + cy.get('[data-test-subj="create-role"]').click(); + cy.contains('h1', 'Create Role'); + cy.get('[data-test-subj="name-text"]').focus().type('9202-role'); + cy.get('[data-test-subj="comboBoxToggleListButton"]').first().click(); + cy.get('button[title="manage_snapshots"]').should('be.visible'); + cy.get('button[title="manage_snapshots"]').click({ force: true }); + + cy.get('[data-test-subj="comboBoxInput"]').first().should('contain', 'manage_snapshots'); + cy.get('[data-test-subj="create-or-update-role"]').click(); + + cy.get('.euiToastHeader__title').should('contain', 'Role "9202-role" successfully created'); + closeToast(); + + // role exists on the remote + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/roles'); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + cy.get('[data-text="Customization"]').click(); + cy.get('[data-test-subj="filter-custom"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('exist'); + + // Role doesn't exist on local cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('not.exist'); + }); }); diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index 0546259fe..38b101fcc 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -741,4 +741,172 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { expect(checkAuditLogSettingsRemote.status).toEqual(200); expect(checkAuditLogSettingsRemote.body.config.enabled).toEqual(false); }); + + it('CRUD Roles for external datasource', async () => { + const rolesEntity = 'roles'; + const rolesMappingEntity = 'rolesmapping'; + const testRoleName = `test_role_${Date.now()}`; + + const payload = { + cluster_permissions: ['cluster_manage_pipelines'], + index_permissions: [ + { + index_patterns: ['*'], + dls: '', + fls: [], + masked_fields: [], + allowed_actions: ['data_access'], + }, + ], + tenant_permissions: [ + { + tenant_patterns: ['global_tenant'], + allowed_actions: ['kibana_all_write'], + }, + ], + }; + + const createRoleRespone = await createOrUpdateEntityAsAdminWithDataSource( + root, + rolesEntity, + testRoleName, + payload, + dataSourceId + ); + expect(createRoleRespone.status).toEqual(200); + + const getRolesResponse = await getAllEntitiesAsAdminWithDataSource( + root, + rolesEntity, + dataSourceId + ); + expect(getRolesResponse.status).toEqual(200); + expect(getRolesResponse.body.data?.hasOwnProperty(testRoleName)).toBe(true); + expect(getRolesResponse.body.data[testRoleName].cluster_permissions).toContain( + 'cluster_manage_pipelines' + ); + + // verify that this AG is not created in Local Cluster + const getRolesResponseLocalCluster = await getAllEntitiesAsAdminWithDataSource( + root, + rolesEntity, + '' + ); + expect(getRolesResponseLocalCluster.status).toEqual(200); + expect(getRolesResponseLocalCluster.body.data?.hasOwnProperty(testRoleName)).toBe(false); + + // Update + const updatePayload = { + cluster_permissions: ['cluster_manage_pipelines', 'manage_snapshots'], + index_permissions: [ + { + index_patterns: ['*'], + dls: '', + fls: [], + masked_fields: [], + allowed_actions: ['data_access'], + }, + ], + tenant_permissions: [ + { + tenant_patterns: ['global_tenant'], + allowed_actions: ['kibana_all_write'], + }, + ], + }; + const updateRoleResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + rolesEntity, + testRoleName, + updatePayload, + dataSourceId + ); + expect(updateRoleResponse.status).toEqual(200); + + const getUpdatedRoleResponse = await getAllEntitiesAsAdminWithDataSource( + root, + rolesEntity, + dataSourceId + ); + expect(getUpdatedRoleResponse.status).toEqual(200); + expect(getUpdatedRoleResponse.body.data?.hasOwnProperty(testRoleName)).toBe(true); + expect(getUpdatedRoleResponse.body.data[testRoleName].cluster_permissions).toContain( + 'manage_snapshots' + ); + + // update RoleMapping + const getRoleMappingResponse = await getEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + dataSourceId + ); + expect(getRoleMappingResponse.status).toEqual(404); // no mapping at first + + const rolesMappingPayload = { + users: ['admin'], + backend_roles: [], + hosts: [], + }; + + const createRoleMappingResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + rolesMappingPayload, + dataSourceId + ); + expect(createRoleMappingResponse.status).toEqual(200); + + const getUpdatedRoleMappingResponse = await getEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + dataSourceId + ); + expect(getUpdatedRoleMappingResponse.status).toEqual(200); + expect(getUpdatedRoleMappingResponse.body.users).toContain('admin'); + + // delete RoleMapping + const getRolesMappingToDelete = await getEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + dataSourceId + ); + expect(getRolesMappingToDelete.status).toEqual(200); + + const deleteRoleMapping = await deleteEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + dataSourceId + ); + expect(deleteRoleMapping.status).toEqual(200); + + const getRoleMappingUpdated = await getEntityAsAdminWithDataSource( + root, + rolesMappingEntity, + testRoleName, + dataSourceId + ); + expect(getRoleMappingUpdated.status).toEqual(404); // no mapping after first + + // Delete + const deleteRolesResponse = await deleteEntityAsAdminWithDataSource( + root, + rolesEntity, + testRoleName, + dataSourceId + ); + expect(deleteRolesResponse.status).toEqual(200); + + const getDeletedRoleResponse = await getAllEntitiesAsAdminWithDataSource( + root, + rolesEntity, + dataSourceId + ); + expect(getDeletedRoleResponse.status).toEqual(200); + expect(getDeletedRoleResponse.body.data?.hasOwnProperty(testRoleName)).toBe(false); + }); }); From 0a57e753ce2d72fc458a87b8a0613fe8b6969330 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 4 Apr 2024 13:54:20 -0400 Subject: [PATCH 08/22] Support multi datasources on service accounts page (#1870) * Add selector on service accounts page Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Fix test for service accounts Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- .../apps/configuration/panels/service-account-list.tsx | 8 ++++++++ .../multi_datasources_enabled.spec.js | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/public/apps/configuration/panels/service-account-list.tsx b/public/apps/configuration/panels/service-account-list.tsx index 9feebba4a..c775f7589 100644 --- a/public/apps/configuration/panels/service-account-list.tsx +++ b/public/apps/configuration/panels/service-account-list.tsx @@ -42,6 +42,8 @@ import { ExternalLink, tableItemsUIProps, truncatedListView } from '../utils/dis import { getUserList, InternalUsersListing } from '../utils/internal-user-list-utils'; import { showTableStatusMessage } from '../utils/loading-spinner-utils'; import { buildHashUrl } from '../utils/url-builder'; +import { LocalCluster } from '../app-router'; +import { SecurityPluginTopNavMenu } from '../top-nav-menu'; export function dictView(items: Dictionary) { if (isEmpty(items)) { @@ -159,6 +161,12 @@ export function ServiceAccountList(props: AppDependencies) { return ( <> + {}} + selectedDataSource={LocalCluster} + />

Service accounts

diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 229fa448d..663c73e02 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -193,6 +193,16 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('be.disabled'); }); + it('Checks Service Accounts Tab', () => { + // Datasource is locked to local cluster for service accounts tab + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/serviceAccounts'); + cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('be.disabled'); + }); + it('Checks Audit Logs Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auditLogging'); cy.get('[data-test-subj="general-settings"]').should('exist'); From 1ddef9bcabeb7130d02af358237d6257b7daeb59 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 11 Apr 2024 11:10:57 -0400 Subject: [PATCH 09/22] Merge main Signed-off-by: Darshit Chanpura --- .eslintrc.js | 4 ++ .github/CODEOWNERS | 2 +- .github/actions/run-cypress-tests/action.yml | 2 +- .github/workflows/integration-test.yml | 8 ++-- .husky/pre-commit | 4 ++ MAINTAINERS.md | 1 + package.json | 13 ++++-- public/apps/account/tenant-switch-panel.tsx | 2 +- public/apps/account/test/plugin.test.tsx | 43 +++++++++++++++-- public/apps/account/utils.tsx | 24 ++-------- public/apps/configuration/constants.tsx | 46 +++++++++++++++++-- .../apps/configuration/panels/get-started.tsx | 3 +- .../test/backend-role-panel.test.tsx | 4 +- .../panels/role-edit/test/role-edit.test.tsx | 4 +- .../role-mapping/role-edit-mapped-user.tsx | 2 +- .../panels/role-view/role-view.tsx | 2 +- .../panels/role-view/tenants-panel.tsx | 2 +- .../panels/service-account-list.tsx | 2 +- .../panels/tenant-list/configure_tab1.tsx | 8 ++-- .../panels/tenant-list/manage_tab.tsx | 4 +- .../panels/tenant-list/tenant-list.tsx | 7 ++- .../__snapshots__/tenant-list.test.tsx.snap | 4 +- .../panels/test/get-started.test.tsx | 2 +- .../configuration/test/app-router.test.tsx | 1 - .../utils/tenancy-config_util.tsx | 2 +- .../apps/configuration/utils/tenant-utils.tsx | 2 - .../utils/test/password-edit-panel.test.tsx | 1 - public/apps/login/login-app.tsx | 2 +- public/apps/login/login-page.tsx | 25 ++++++++-- public/apps/login/test/login-page.test.tsx | 42 ++++++++++++++++- public/utils/logout-utils.tsx | 6 ++- ...ashboards-plugin.release-notes-2.13.0.0.md | 12 +++++ server/auth/types/jwt/jwt_auth.ts | 1 - server/auth/types/jwt/jwt_helper.test.ts | 6 +-- server/auth/types/multiple/multi_auth.ts | 2 +- server/auth/types/openid/openid_auth.ts | 3 +- server/auth/types/openid/routes.ts | 1 - server/auth/types/proxy/proxy_auth.ts | 1 - server/backend/opensearch_security_client.ts | 1 - test/constant.ts | 2 +- .../security_entity_api.test.ts | 6 +-- yarn.lock | 17 +++++++ 42 files changed, 237 insertions(+), 89 deletions(-) create mode 100755 .husky/pre-commit create mode 100644 release-notes/opensearch-security-dashboards-plugin.release-notes-2.13.0.0.md diff --git a/.eslintrc.js b/.eslintrc.js index 5e176b8bb..1d8ac940b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { }, plugins: [ 'cypress', + "unused-imports" ], rules: { // "@osd/eslint/require-license-header": "off" @@ -44,6 +45,9 @@ module.exports = { 'cypress/assertion-before-screenshot': 'warn', 'cypress/no-force': 'warn', 'cypress/no-async-tests': 'error', + // Unused imports and variables rules + "no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", }, overrides: [ { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 547813578..468826191 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @cliu123 @cwperks @DarshitChanpura @davidlago @peternied @RyanL1997 @scrawfor99 +* @cliu123 @cwperks @DarshitChanpura @davidlago @derek-ho @peternied @RyanL1997 @scrawfor99 diff --git a/.github/actions/run-cypress-tests/action.yml b/.github/actions/run-cypress-tests/action.yml index 678634455..74e001ff7 100644 --- a/.github/actions/run-cypress-tests/action.yml +++ b/.github/actions/run-cypress-tests/action.yml @@ -56,7 +56,7 @@ runs: if: ${{ runner.os == 'Linux' }} run: | cd ./OpenSearch-Dashboards/plugins/security-dashboards-plugin - yarn pretest:jest_server + yarn runIdp shell: bash - name: Run OpenSearch Dashboards with provided configuration diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 2a15958df..e35ae5363 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,7 +6,7 @@ env: TEST_BROWSER_HEADLESS: 1 CI: 1 PLUGIN_NAME: opensearch-security - OPENSEARCH_INITIAL_ADMIN_PASSWORD: admin + OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! jobs: tests: @@ -98,7 +98,8 @@ jobs: run: | echo "check if opensearch is ready" curl -XGET https://localhost:9200 -u 'admin:${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }}' -k - yarn test:jest_server --coverage + ADMIN_PASSWORD=${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} yarn test:jest_server --coverage + shell: bash working-directory: ${{ steps.install-dashboards.outputs.plugin-directory }} - name: Run integration tests on Windows @@ -106,5 +107,6 @@ jobs: run: | echo "check if opensearch is ready" curl -XGET https://localhost:9200 -u 'admin:${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }}' -k - node .\test\run_jest_tests.js --runInBand --detectOpenHandles --forceExit --config .\test\jest.config.server.js + export ADMIN_PASSWORD=${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} && node ./test/run_jest_tests.js --runInBand --detectOpenHandles --forceExit --config ./test/jest.config.server.js + shell: bash working-directory: ${{ steps.install-dashboards.outputs.plugin-directory }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..deae1a9ce --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint:es --fix \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f7d069cd1..4e21da685 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -13,6 +13,7 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Craig Perkins | [cwperks](https://github.com/cwperks) | Amazon | | Ryan Liang | [RyanL1997](https://github.com/RyanL1997) | Amazon | | Stephen Crawford | [scrawfor99](https://github.com/scrawfor99) | Amazon | +| Derek Ho | [derek-ho](https://github.com/derek-ho) | Amazon | ## Emeritus diff --git a/package.json b/package.json index e888182d9..acd56721e 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "lint:es": "node ../../scripts/eslint", "lint:style": "node ../../scripts/stylelint", "lint": "yarn run lint:es && yarn run lint:style", - "pretest:jest_server": "node ./test/jest_integration/runIdpServer.js &", - "test:jest_server": "node ./test/run_jest_tests.js --config ./test/jest.config.server.js", - "test:jest_ui": "node ./test/run_jest_tests.js --config ./test/jest.config.ui.js" + "runIdp": "node ./test/jest_integration/runIdpServer.js &", + "test:jest_server": "ADMIN_PASSWORD=$ADMIN_PASSWORD node ./test/run_jest_tests.js --config ./test/jest.config.server.js", + "test:jest_ui": "node ./test/run_jest_tests.js --config ./test/jest.config.ui.js", + "prepare": "husky install" }, "devDependencies": { "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards", @@ -34,7 +35,9 @@ "saml-idp": "^1.2.1", "selfsigned": "^2.0.1", "typescript": "4.0.2", - "eslint-plugin-cypress": "^2.8.1" + "eslint-plugin-cypress": "^2.8.1", + "eslint-plugin-unused-imports": "3.1.0", + "husky": "^8.0.0" }, "dependencies": { "@hapi/cryptiles": "5.0.0", @@ -46,4 +49,4 @@ "glob-parent": "^5.1.2", "debug": "^4.3.4" } -} \ No newline at end of file +} diff --git a/public/apps/account/tenant-switch-panel.tsx b/public/apps/account/tenant-switch-panel.tsx index 2768d9eb0..103647ec8 100755 --- a/public/apps/account/tenant-switch-panel.tsx +++ b/public/apps/account/tenant-switch-panel.tsx @@ -40,7 +40,7 @@ import { } from '../configuration/utils/tenant-utils'; import { fetchAccountInfo } from './utils'; import { constructErrorMessageAndLog } from '../error-utils'; -import { getSavedTenant, setSavedTenant } from '../../utils/storage-utils'; +import { setSavedTenant } from '../../utils/storage-utils'; import { getDashboardsInfo } from '../../utils/dashboards-info-utils'; interface TenantSwitchPanelProps { diff --git a/public/apps/account/test/plugin.test.tsx b/public/apps/account/test/plugin.test.tsx index a792eea30..f3e7d4a76 100644 --- a/public/apps/account/test/plugin.test.tsx +++ b/public/apps/account/test/plugin.test.tsx @@ -13,15 +13,39 @@ * permissions and limitations under the License. */ +import { LOGIN_PAGE_URI } from '../../../../common'; import { interceptError } from '../../../utils/logout-utils'; import { setShouldShowTenantPopup } from '../../../utils/storage-utils'; -import { LOGIN_PAGE_URI } from '../../../../common'; jest.mock('../../../utils/storage-utils', () => ({ setShouldShowTenantPopup: jest.fn(), })); +interface LooseObject { + [key: string]: any; +} + +// Mock sessionStorage +const sessionStorageMock = (() => { + let store = {} as LooseObject; + return { + clear() { + store = {}; + }, + }; +})(); + +Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock }); + describe('Intercept error handler', () => { + beforeEach(() => { + jest.spyOn(window.sessionStorage, 'clear'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + const fakeError401 = { response: { status: 401, @@ -34,15 +58,28 @@ describe('Intercept error handler', () => { }, }; - it('Intercept error handler Should call setShouldShowTenantPopup on session timeout', () => { + it('Intercept error handler should call setShouldShowTenantPopup on session timeout', () => { const sessionTimeoutFn = interceptError(LOGIN_PAGE_URI, window); sessionTimeoutFn(fakeError401, null); expect(setShouldShowTenantPopup).toBeCalledTimes(1); + expect(sessionStorage.clear).toBeCalledTimes(1); + }); + + it('Intercept error handler should clear the session', () => { + const sessionTimeoutFn = interceptError(LOGIN_PAGE_URI, window); + sessionTimeoutFn(fakeError401, null); + expect(sessionStorage.clear).toBeCalledTimes(1); }); - it('Intercept error handler Should not call setShouldShowTenantPopup on session timeout', () => { + it('Intercept error handler should not call setShouldShowTenantPopup on session timeout', () => { const sessionTimeoutFn = interceptError(LOGIN_PAGE_URI, window); sessionTimeoutFn(fakeError400, null); expect(setShouldShowTenantPopup).toBeCalledTimes(0); }); + + it('Intercept error handler should not clear the session', () => { + const sessionTimeoutFn = interceptError(LOGIN_PAGE_URI, window); + sessionTimeoutFn(fakeError400, null); + expect(sessionStorage.clear).toBeCalledTimes(0); + }); }); diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index e411895bc..2f9f25eee 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -14,16 +14,11 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { - API_AUTH_LOGOUT, - LOGIN_PAGE_URI, - OPENID_AUTH_LOGOUT, - SAML_AUTH_LOGOUT, -} from '../../../common'; +import { API_AUTH_LOGOUT } from '../../../common'; +import { setShouldShowTenantPopup } from '../../utils/storage-utils'; +import { httpGet, httpGetWithIgnores, httpPost } from '../configuration/utils/request-utils'; import { API_ENDPOINT_ACCOUNT_INFO } from './constants'; import { AccountInfo } from './types'; -import { httpGet, httpGetWithIgnores, httpPost } from '../configuration/utils/request-utils'; -import { setShouldShowTenantPopup } from '../../utils/storage-utils'; export function fetchAccountInfo(http: HttpStart): Promise { return httpGet({ http, url: API_ENDPOINT_ACCOUNT_INFO }); @@ -45,19 +40,6 @@ export async function logout(http: HttpStart, logoutUrl?: string): Promise logoutUrl || `${http.basePath.serverBasePath}/app/login?nextUrl=${nextUrl}`; } -export async function samlLogout(http: HttpStart): Promise { - // This will ensure tenancy is picked up from local storage in the next login. - setShouldShowTenantPopup(null); - window.location.href = `${http.basePath.serverBasePath}${SAML_AUTH_LOGOUT}`; -} - -export async function openidLogout(http: HttpStart): Promise { - // This will ensure tenancy is picked up from local storage in the next login. - setShouldShowTenantPopup(null); - sessionStorage.clear(); - window.location.href = `${http.basePath.serverBasePath}${OPENID_AUTH_LOGOUT}`; -} - export async function externalLogout(http: HttpStart, logoutEndpoint: string): Promise { // This will ensure tenancy is picked up from local storage in the next login. setShouldShowTenantPopup(null); diff --git a/public/apps/configuration/constants.tsx b/public/apps/configuration/constants.tsx index 9df5f8285..93b36aa96 100644 --- a/public/apps/configuration/constants.tsx +++ b/public/apps/configuration/constants.tsx @@ -145,24 +145,64 @@ export const CLUSTER_PERMISSIONS: string[] = [ 'cluster:admin/opensearch/ql/async_query/result', 'cluster:admin/opensearch/ql/async_query/delete', 'cluster:admin/opensearch/ppl', + 'cluster:admin/opensearch/ml/agents/delete', + 'cluster:admin/opensearch/ml/agents/get', + 'cluster:admin/opensearch/ml/agents/register', + 'cluster:admin/opensearch/ml/agents/search', + 'cluster:admin/opensearch/ml/config/get', + 'cluster:admin/opensearch/ml/create_connector', + 'cluster:admin/opensearch/ml/connectors/get', + 'cluster:admin/opensearch/ml/connectors/search', + 'cluster:admin/opensearch/ml/connectors/update', + 'cluster:admin/opensearch/ml/controllers/create', + 'cluster:admin/opensearch/ml/controllers/delete', + 'cluster:admin/opensearch/ml/controllers/deploy', + 'cluster:admin/opensearch/ml/controllers/get', + 'cluster:admin/opensearch/ml/controllers/undeploy', + 'cluster:admin/opensearch/ml/controllers/update', 'cluster:admin/opensearch/ml/create_model_meta', 'cluster:admin/opensearch/ml/execute', - 'cluster:admin/opensearch/ml/load_model', - 'cluster:admin/opensearch/ml/load_model_on_nodes', + 'cluster:admin/opensearch/ml/deploy_model', + 'cluster:admin/opensearch/ml/deploy_model_on_nodes', + 'cluster:admin/opensearch/ml/memory/conversation/get', + 'cluster:admin/opensearch/ml/memory/conversation/interaction/search', + 'cluster:admin/opensearch/ml/memory/conversation/delete', + 'cluster:admin/opensearch/ml/memory/conversation/list', + 'cluster:admin/opensearch/ml/memory/conversation/search', + 'cluster:admin/opensearch/ml/memory/conversation/create', + 'cluster:admin/opensearch/ml/memory/conversation/update', + 'cluster:admin/opensearch/ml/memory/interaction/create', + 'cluster:admin/opensearch/ml/memory/interaction/update', + 'cluster:admin/opensearch/ml/memory/interaction/get', + 'cluster:admin/opensearch/ml/memory/interaction/list', + 'cluster:admin/opensearch/ml/memory/trace/get', + 'cluster:admin/opensearch/ml/model_groups/delete', + 'cluster:admin/opensearch/ml/model_groups/get', + 'cluster:admin/opensearch/ml/model_groups/search', + 'cluster:admin/opensearch/ml/register_model_group', + 'cluster:admin/opensearch/ml/update_model_group', 'cluster:admin/opensearch/ml/models/delete', 'cluster:admin/opensearch/ml/models/get', 'cluster:admin/opensearch/ml/models/search', + 'cluster:admin/opensearch/ml/models/update', + 'cluster:admin/opensearch/ml/models/update_cache', 'cluster:admin/opensearch/ml/predict', 'cluster:admin/opensearch/ml/profile/nodes', + 'cluster:admin/opensearch/ml/register_model', + 'cluster:admin/opensearch/ml/register_model_meta', 'cluster:admin/opensearch/ml/stats/nodes', 'cluster:admin/opensearch/ml/tasks/delete', 'cluster:admin/opensearch/ml/tasks/get', 'cluster:admin/opensearch/ml/tasks/search', + 'cluster:admin/opensearch/ml/tools/get', + 'cluster:admin/opensearch/ml/tools/list', 'cluster:admin/opensearch/ml/train', 'cluster:admin/opensearch/ml/trainAndPredict', - 'cluster:admin/opensearch/ml/unload_model', + 'cluster:admin/opensearch/ml/undeploy_model', + 'cluster:admin/opensearch/ml/undeploy_models', 'cluster:admin/opensearch/ml/upload_model', 'cluster:admin/opensearch/ml/upload_model_chunk', + 'cluster:admin/opensearch/mlinternal/forward', 'cluster:admin/opensearch/observability/create', 'cluster:admin/opensearch/observability/delete', 'cluster:admin/opensearch/observability/get', diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index d55c8aefa..dd638f5ce 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -26,9 +26,8 @@ import { EuiTitle, EuiGlobalToastList, } from '@elastic/eui'; -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; -import { DataSourceOption } from '../../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; import { AppDependencies } from '../../types'; import { buildHashUrl } from '../utils/url-builder'; import { Action } from '../types'; diff --git a/public/apps/configuration/panels/internal-user-edit/test/backend-role-panel.test.tsx b/public/apps/configuration/panels/internal-user-edit/test/backend-role-panel.test.tsx index 268ca7759..d24f81d6e 100644 --- a/public/apps/configuration/panels/internal-user-edit/test/backend-role-panel.test.tsx +++ b/public/apps/configuration/panels/internal-user-edit/test/backend-role-panel.test.tsx @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -import { EuiFieldText, EuiFlexGroup, EuiFormRow } from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; +import { EuiFieldText, EuiFlexGroup } from '@elastic/eui'; +import { shallow } from 'enzyme'; import React from 'react'; import { appendElementToArray, diff --git a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx index 68bfd602b..81ac845be 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { EuiButton, EuiFieldText } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; import { updateRole } from '../../../utils/role-detail-utils'; @@ -21,7 +21,7 @@ import { setCrossPageToast } from '../../../utils/storage-utils'; import { fetchTenantNameList } from '../../../utils/tenant-utils'; import { ClusterPermissionPanel } from '../cluster-permission-panel'; import { IndexPermissionPanel } from '../index-permission-panel'; -import { getSuccessToastMessage, RoleEdit } from '../role-edit'; +import { RoleEdit } from '../role-edit'; import { TenantPanel } from '../tenant-panel'; jest.mock('../../../utils/role-detail-utils', () => ({ diff --git a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx index c9f9d24b6..a2ae92c60 100644 --- a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx +++ b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx @@ -23,7 +23,7 @@ import { EuiTitle, EuiGlobalToastList, } from '@elastic/eui'; -import React, { useState, useEffect, useContext } from 'react'; +import React, { useState, useContext } from 'react'; import { BreadcrumbsPageDependencies } from '../../../types'; import { InternalUsersPanel } from './users-panel'; import { diff --git a/public/apps/configuration/panels/role-view/role-view.tsx b/public/apps/configuration/panels/role-view/role-view.tsx index 5f1ba3e5f..d82710486 100644 --- a/public/apps/configuration/panels/role-view/role-view.tsx +++ b/public/apps/configuration/panels/role-view/role-view.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState, useEffect, useContext } from 'react'; +import React, { useState, useContext } from 'react'; import { EuiButton, diff --git a/public/apps/configuration/panels/role-view/tenants-panel.tsx b/public/apps/configuration/panels/role-view/tenants-panel.tsx index 45d19bb2b..d0cde8cd9 100644 --- a/public/apps/configuration/panels/role-view/tenants-panel.tsx +++ b/public/apps/configuration/panels/role-view/tenants-panel.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { EuiInMemoryTable, EuiLink, diff --git a/public/apps/configuration/panels/service-account-list.tsx b/public/apps/configuration/panels/service-account-list.tsx index c775f7589..2ebf47662 100644 --- a/public/apps/configuration/panels/service-account-list.tsx +++ b/public/apps/configuration/panels/service-account-list.tsx @@ -29,7 +29,7 @@ import { EuiTitle, Query, } from '@elastic/eui'; -import { Dictionary, difference, isEmpty, map } from 'lodash'; +import { Dictionary, isEmpty, map } from 'lodash'; import React, { useState } from 'react'; import { getAuthInfo } from '../../../utils/auth-info-utils'; import { AppDependencies } from '../../types'; diff --git a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx index dd8686664..dafd153d6 100644 --- a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx +++ b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx @@ -328,7 +328,7 @@ export function ConfigureTab1(props: AppDependencies) { -

Multi-tenancy

+

Dashboards multi-tenancy

@@ -360,7 +360,7 @@ export function ConfigureTab1(props: AppDependencies) { -

Tenants

+

Dashboards tenants

{' '} - Global tenant is shared amaong all Dashboards users and cannot be disabled.{' '} + Global tenant is shared among all Dashboards users and cannot be disabled.{' '} } className="described-form-group2" @@ -407,7 +407,7 @@ export function ConfigureTab1(props: AppDependencies) { -

Default tenant

+

Dashboards default tenant

- Tenants + Dashboards tenants {' '} ({Query.execute(query || '', tenantData).length}) diff --git a/public/apps/configuration/panels/tenant-list/tenant-list.tsx b/public/apps/configuration/panels/tenant-list/tenant-list.tsx index 3fd5b89da..0b97c6ae1 100644 --- a/public/apps/configuration/panels/tenant-list/tenant-list.tsx +++ b/public/apps/configuration/panels/tenant-list/tenant-list.tsx @@ -23,16 +23,15 @@ import { EuiButton, } from '@elastic/eui'; import { Route } from 'react-router-dom'; -import React, { useState, useMemo, useCallback, useContext } from 'react'; +import React, { useState, useMemo } from 'react'; import { ManageTab } from './manage_tab'; import { ConfigureTab1 } from './configure_tab1'; import { AppDependencies } from '../../../types'; import { ExternalLink } from '../../utils/display-utils'; -import { displayBoolean } from '../../utils/display-utils'; import { DocLinks } from '../../constants'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; import { TenantInstructionView } from './tenant-instruction-view'; -import { DataSourceContext, LocalCluster } from '../../app-router'; +import { LocalCluster } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; interface TenantListProps extends AppDependencies { @@ -144,7 +143,7 @@ export function TenantList(props: TenantListProps) { /> -

Multi-tenancy

+

Dashboards multi-tenancy

diff --git a/public/apps/configuration/panels/tenant-list/test/__snapshots__/tenant-list.test.tsx.snap b/public/apps/configuration/panels/tenant-list/test/__snapshots__/tenant-list.test.tsx.snap index 64c1e1b64..c817212aa 100644 --- a/public/apps/configuration/panels/tenant-list/test/__snapshots__/tenant-list.test.tsx.snap +++ b/public/apps/configuration/panels/tenant-list/test/__snapshots__/tenant-list.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Tenant list Action menu click Duplicate click 1`] = ` size="s" >

- Tenants + Dashboards tenants @@ -198,7 +198,7 @@ exports[`Tenant list Action menu click Edit click 1`] = ` size="s" >

- Tenants + Dashboards tenants diff --git a/public/apps/configuration/panels/test/get-started.test.tsx b/public/apps/configuration/panels/test/get-started.test.tsx index 09e64578d..00e73a542 100644 --- a/public/apps/configuration/panels/test/get-started.test.tsx +++ b/public/apps/configuration/panels/test/get-started.test.tsx @@ -19,7 +19,7 @@ import { EuiSteps } from '@elastic/eui'; import { Action } from '../../types'; import { ResourceType } from '../../../../../common'; import { buildHashUrl } from '../../utils/url-builder'; -import { GetStarted, getClusterInfoIfEnabled } from '../get-started'; +import { GetStarted } from '../get-started'; import * as ToastUtils from '../../utils/toast-utils'; // Import all functions from toast-utils import * as RequestUtils from '../../utils/request-utils'; // Import all functions from request-utils diff --git a/public/apps/configuration/test/app-router.test.tsx b/public/apps/configuration/test/app-router.test.tsx index 990c448a3..f93c1014d 100644 --- a/public/apps/configuration/test/app-router.test.tsx +++ b/public/apps/configuration/test/app-router.test.tsx @@ -15,7 +15,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { AppRouter } from '../app-router'; describe('SecurityPluginTopNavMenu', () => { diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index 70ab84c63..adff9d165 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -15,7 +15,7 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_TENANCY_CONFIGS } from '../constants'; -import { httpGet, httpPut, httpPost } from './request-utils'; +import { httpGet, httpPost } from './request-utils'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; export async function updateTenancyConfig(http: HttpStart, updateObject: TenancyConfigSettings) { diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index 329acbd44..c467c809e 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -39,7 +39,6 @@ import { import { httpDelete, httpGet, httpPost, httpPut } from './request-utils'; import { getResourceUrl } from './resource-utils'; import { - API_ENDPOINT_DASHBOARDSINFO, DEFAULT_TENANT, GLOBAL_TENANT_RENDERING_TEXT, GLOBAL_TENANT_SYMBOL, @@ -47,7 +46,6 @@ import { isGlobalTenant, isRenderingPrivateTenant, PRIVATE_TENANT_RENDERING_TEXT, - SAML_AUTH_LOGIN, } from '../../../../common'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; diff --git a/public/apps/configuration/utils/test/password-edit-panel.test.tsx b/public/apps/configuration/utils/test/password-edit-panel.test.tsx index 49ab94b65..247a88d0b 100644 --- a/public/apps/configuration/utils/test/password-edit-panel.test.tsx +++ b/public/apps/configuration/utils/test/password-edit-panel.test.tsx @@ -16,7 +16,6 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { PasswordEditPanel } from '../password-edit-panel'; -import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; const mockDashboardsInfo = { multitenancy_enabled: true, diff --git a/public/apps/login/login-app.tsx b/public/apps/login/login-app.tsx index 340b45eec..33cb1ebe2 100644 --- a/public/apps/login/login-app.tsx +++ b/public/apps/login/login-app.tsx @@ -15,7 +15,7 @@ import './_index.scss'; // @ts-ignore : Component not used -import React, { Component } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { LoginPage } from './login-page'; diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 70d894781..a22a36dc7 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -35,6 +35,7 @@ import { OPENID_AUTH_LOGIN_WITH_FRAGMENT, SAML_AUTH_LOGIN_WITH_FRAGMENT, } from '../../../common'; +import { getSavedTenant } from '../../utils/storage-utils'; interface LoginPageDeps { http: CoreStart['http']; @@ -49,8 +50,7 @@ interface LoginButtonConfig { buttonstyle: string; } -function redirect(serverBasePath: string) { - // navigate to nextUrl +export function getNextPath(serverBasePath: string) { const urlParams = new URLSearchParams(window.location.search); let nextUrl = urlParams.get('nextUrl'); if (!nextUrl || nextUrl.toLowerCase().includes('//')) { @@ -58,7 +58,26 @@ function redirect(serverBasePath: string) { // redirect to '/'. nextUrl = serverBasePath + '/'; } - window.location.href = nextUrl + window.location.hash; + const savedTenant = getSavedTenant(); + const url = new URL( + window.location.protocol + '//' + window.location.host + nextUrl + window.location.hash + ); + if ( + !!savedTenant && + !( + url.searchParams.has('security_tenant') || + url.searchParams.has('securitytenant') || + url.searchParams.has('securityTenant_') + ) + ) { + url.searchParams.append('security_tenant', savedTenant); + } + return url.pathname + url.search + url.hash; +} + +function redirect(serverBasePath: string) { + // navigate to nextUrl + window.location.href = getNextPath(serverBasePath); } export function extractNextUrlFromWindowLocation(): string { diff --git a/public/apps/login/test/login-page.test.tsx b/public/apps/login/test/login-page.test.tsx index f21a39e5a..8d1c76358 100644 --- a/public/apps/login/test/login-page.test.tsx +++ b/public/apps/login/test/login-page.test.tsx @@ -16,11 +16,12 @@ import { shallow } from 'enzyme'; import React from 'react'; import { ClientConfigType } from '../../../types'; -import { LoginPage, extractNextUrlFromWindowLocation } from '../login-page'; +import { LoginPage, extractNextUrlFromWindowLocation, getNextPath } from '../login-page'; import { validateCurrentPassword } from '../../../utils/login-utils'; import { API_AUTH_LOGOUT } from '../../../../common'; import { chromeServiceMock } from '../../../../../../src/core/public/mocks'; import { AuthType } from '../../../../common'; +import { setSavedTenant } from '../../../utils/storage-utils'; jest.mock('../../../utils/login-utils', () => ({ validateCurrentPassword: jest.fn(), @@ -85,6 +86,45 @@ describe('test extractNextUrlFromWindowLocation', () => { }); }); +describe('test redirect', () => { + test('extract redirect excludes security_tenant when no tenant in local storage', () => { + // Trick to mock window.location + const originalLocation = window.location; + delete window.location; + window.location = new URL('http://localhost:5601/app/login?nextUrl=%2Fapp%2Fdashboards') as any; + setSavedTenant(null); + const nextPath = getNextPath(''); + expect(nextPath).toEqual('/app/dashboards'); + window.location = originalLocation; + }); + + test('extract redirect includes security_tenant when tenant in local storage', () => { + const originalLocation = window.location; + delete window.location; + window.location = new URL('http://localhost:5601/app/login?nextUrl=%2Fapp%2Fdashboards'); + setSavedTenant('custom'); + const nextPath = getNextPath(''); + expect(nextPath).toEqual('/app/dashboards?security_tenant=custom'); + setSavedTenant(null); + window.location = originalLocation; + }); + + test('extract redirect includes security_tenant when tenant in local storage, existing url params and hash', () => { + const originalLocation = window.location; + delete window.location; + window.location = new URL( + "http://localhost:5601/app/login?nextUrl=%2Fapp%2Fdashboards?param1=value1#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-24h,to:now))&_a=(description:'Analyze%20mock%20flight%20data%20for%20OpenSearch-Air,%20Logstash%20Airways,%20OpenSearch%20Dashboards%20Airlines%20and%20BeatsWest',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),query:(language:kuery,query:''),timeRestore:!t,title:'%5BFlights%5D%20Global%20Flight%20Dashboard',viewMode:view)" + ); + setSavedTenant('custom'); + const nextPath = getNextPath(''); + expect(nextPath).toEqual( + "/app/dashboards?param1=value1&security_tenant=custom#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-24h,to:now))&_a=(description:'Analyze%20mock%20flight%20data%20for%20OpenSearch-Air,%20Logstash%20Airways,%20OpenSearch%20Dashboards%20Airlines%20and%20BeatsWest',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),query:(language:kuery,query:''),timeRestore:!t,title:'%5BFlights%5D%20Global%20Flight%20Dashboard',viewMode:view)" + ); + setSavedTenant(null); + window.location = originalLocation; + }); +}); + describe('Login page', () => { let chrome: ReturnType; const mockHttpStart = { diff --git a/public/utils/logout-utils.tsx b/public/utils/logout-utils.tsx index 90f5a75a0..d195cc79f 100644 --- a/public/utils/logout-utils.tsx +++ b/public/utils/logout-utils.tsx @@ -13,19 +13,21 @@ * permissions and limitations under the License. */ -import { setShouldShowTenantPopup } from './storage-utils'; import { HttpInterceptorResponseError, HttpStart, IHttpInterceptController, } from '../../../../src/core/public'; -import { CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI, API_ENDPOINT_AUTHTYPE } from '../../common'; +import { API_ENDPOINT_AUTHTYPE, CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI } from '../../common'; import { httpGet } from '../apps/configuration/utils/request-utils'; +import { setShouldShowTenantPopup } from './storage-utils'; export function interceptError(logoutUrl: string, thisWindow: Window): any { return (httpErrorResponse: HttpInterceptorResponseError, _: IHttpInterceptController) => { if (httpErrorResponse.response?.status === 401) { setShouldShowTenantPopup(null); + // Clear everything in the sessionStorage since they can contain sensitive information + sessionStorage.clear(); if ( !( thisWindow.location.pathname.toLowerCase().includes(LOGIN_PAGE_URI) || diff --git a/release-notes/opensearch-security-dashboards-plugin.release-notes-2.13.0.0.md b/release-notes/opensearch-security-dashboards-plugin.release-notes-2.13.0.0.md new file mode 100644 index 000000000..4089ab028 --- /dev/null +++ b/release-notes/opensearch-security-dashboards-plugin.release-notes-2.13.0.0.md @@ -0,0 +1,12 @@ +## Version 2.13.0.0 + +Compatible with OpenSearch-Dashboards 2.13.0 + +### Enhancements +* Clear the contents of opensearch_dashboards prior to putting settings ([#1781](https://github.com/opensearch-project/security-dashboards-plugin/pull/1781)) +* Add loose flag to OSD bootstrap ([#1789](https://github.com/opensearch-project/security-dashboards-plugin/pull/1789)) +* Hide tenant when disabled in the account nav button popover ([#1792](https://github.com/opensearch-project/security-dashboards-plugin/pull/1792)) +* Use start-opensearch and setup-opensearch-dashboards actions ([#1808](https://github.com/opensearch-project/security-dashboards-plugin/pull/1808)) +* Fix cookie expiry issues from IDP/JWT auth methods, disables keepalive for JWT/IDP ([#1806](https://github.com/opensearch-project/security-dashboards-plugin/pull/1806)) +* Copy tenant with Short URL ([#1812](https://github.com/opensearch-project/security-dashboards-plugin/pull/1812)) +* Add toast handling for purge cache action ([#1827](https://github.com/opensearch-project/security-dashboards-plugin/pull/1827)) diff --git a/server/auth/types/jwt/jwt_auth.ts b/server/auth/types/jwt/jwt_auth.ts index da1c13dc6..cba89b79b 100644 --- a/server/auth/types/jwt/jwt_auth.ts +++ b/server/auth/types/jwt/jwt_auth.ts @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -import { ParsedUrlQuery } from 'querystring'; import { SessionStorageFactory, IRouter, diff --git a/server/auth/types/jwt/jwt_helper.test.ts b/server/auth/types/jwt/jwt_helper.test.ts index f82621a62..ae9012718 100644 --- a/server/auth/types/jwt/jwt_helper.test.ts +++ b/server/auth/types/jwt/jwt_helper.test.ts @@ -14,7 +14,7 @@ */ import { getAuthenticationHandler } from '../../auth_handler_factory'; -import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS, JwtAuthentication } from './jwt_auth'; +import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS } from './jwt_auth'; import { CoreSetup, ILegacyClusterClient, @@ -215,7 +215,7 @@ describe('test jwt auth library', () => { }); }); // re-import JWTAuth to change cookie splitter to a no-op -/* eslint-disable no-shadow, @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-var-requires */ describe('JWT Expiry Tests', () => { const setExtraAuthStorageMock = jest.fn(); jest.resetModules(); @@ -388,5 +388,5 @@ describe('JWT Expiry Tests', () => { jwtAuth.buildAuthHeaderFromCookie.mockRestore(); }); - /* eslint-enable no-shadow, @typescript-eslint/no-var-requires */ + /* eslint-enable @typescript-eslint/no-var-requires */ }); diff --git a/server/auth/types/multiple/multi_auth.ts b/server/auth/types/multiple/multi_auth.ts index 2bcc5e1ba..b190d9d03 100644 --- a/server/auth/types/multiple/multi_auth.ts +++ b/server/auth/types/multiple/multi_auth.ts @@ -24,7 +24,7 @@ import { } from 'opensearch-dashboards/server'; import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router'; import { SecurityPluginConfigType } from '../../..'; -import { AuthenticationType, IAuthenticationType } from '../authentication_type'; +import { AuthenticationType } from '../authentication_type'; import { ANONYMOUS_AUTH_LOGIN, AuthType, LOGIN_PAGE_URI } from '../../../../common'; import { composeNextUrlQueryParam } from '../../../utils/next_url'; import { MultiAuthRoutes } from './routes'; diff --git a/server/auth/types/openid/openid_auth.ts b/server/auth/types/openid/openid_auth.ts index 61088dfd3..b67e174c8 100644 --- a/server/auth/types/openid/openid_auth.ts +++ b/server/auth/types/openid/openid_auth.ts @@ -39,10 +39,9 @@ import { import { OpenIdAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; import { callTokenEndpoint } from './helper'; -import { composeNextUrlQueryParam } from '../../../utils/next_url'; import { getObjectProperties } from '../../../utils/object_properties_defined'; import { getExpirationDate } from './helper'; -import { AuthType, OPENID_AUTH_LOGIN } from '../../../../common'; +import { AuthType } from '../../../../common'; import { ExtraAuthStorageOptions, getExtraAuthStorageValue, diff --git a/server/auth/types/openid/routes.ts b/server/auth/types/openid/routes.ts index a8dc2e2b1..c23e26b1f 100644 --- a/server/auth/types/openid/routes.ts +++ b/server/auth/types/openid/routes.ts @@ -43,7 +43,6 @@ import { AUTH_GRANT_TYPE, AUTH_RESPONSE_TYPE, OPENID_AUTH_LOGOUT, - LOGIN_PAGE_URI, } from '../../../../common'; import { diff --git a/server/auth/types/proxy/proxy_auth.ts b/server/auth/types/proxy/proxy_auth.ts index b3a97d5ed..312c6dedf 100644 --- a/server/auth/types/proxy/proxy_auth.ts +++ b/server/auth/types/proxy/proxy_auth.ts @@ -30,7 +30,6 @@ import { SecurityPluginConfigType } from '../../..'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { ProxyAuthRoutes } from './routes'; import { AuthenticationType } from '../authentication_type'; -import { isValidTenant } from '../../../multitenancy/tenant_resolver'; export class ProxyAuthentication extends AuthenticationType { private static readonly XFF: string = 'x-forwarded-for'; diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 7897444e4..71a65d205 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -15,7 +15,6 @@ import { ILegacyClusterClient, OpenSearchDashboardsRequest } from '../../../../src/core/server'; import { User } from '../auth/user'; -import { getAuthInfo } from '../../public/utils/auth-info-utils'; import { TenancyConfigSettings } from '../../public/apps/configuration/panels/tenancy-config/types'; export class SecurityClient { diff --git a/test/constant.ts b/test/constant.ts index 713ea05de..d4ab9e3ae 100644 --- a/test/constant.ts +++ b/test/constant.ts @@ -22,7 +22,7 @@ export const ELASTICSEARCH_VERSION: string = opensearchDashboards.version; export const SECURITY_ES_PLUGIN_VERSION: string = version; export const ADMIN_USER: string = 'admin'; -export const ADMIN_PASSWORD: string = 'admin'; +export const ADMIN_PASSWORD: string = process.env.ADMIN_PASSWORD || 'admin'; const ADMIN_USER_PASS: string = `${ADMIN_USER}:${ADMIN_PASSWORD}`; export const ADMIN_CREDENTIALS: string = `Basic ${Buffer.from(ADMIN_USER_PASS).toString('base64')}`; export const AUTHORIZATION_HEADER_NAME: string = 'Authorization'; diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index 38b101fcc..6e03da978 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -34,11 +34,7 @@ import { getEntityAsAdmin, getEntityAsAdminWithDataSource, } from '../helper/entity_operation'; -import { - testAuditLogDisabledSettings, - testAuditLogEnabledSettings, - testAuditLogSettings, -} from './constants'; +import { testAuditLogDisabledSettings, testAuditLogEnabledSettings } from './constants'; describe('start OpenSearch Dashboards server', () => { let root: Root; diff --git a/yarn.lock b/yarn.lock index 2c119c492..59cbbd085 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1729,6 +1729,18 @@ eslint-plugin-cypress@^2.8.1: dependencies: globals "^13.20.0" +eslint-plugin-unused-imports@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz#db015b569d3774e17a482388c95c17bd303bc602" + integrity sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -2443,6 +2455,11 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +husky@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" From 9f350b0742854659256d815aeec92dc10b7b60fb Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 11 Apr 2024 20:37:17 -0400 Subject: [PATCH 10/22] Add cluster info to toasts, adds cluster info to URL (#1877) * Add cluster info to toasts Signed-off-by: Derek Ho * Add URL support Signed-off-by: Derek Ho * Initial work to read/write from url Signed-off-by: Derek Ho * Consume changes from core Signed-off-by: Derek Ho * Lint and remove unused imports Signed-off-by: Derek Ho * Fix edge case and test Signed-off-by: Derek Ho * Refactors tests to accomodate changes to dataSource value consumption from URL Signed-off-by: Darshit Chanpura * Try in headless mode Signed-off-by: Derek Ho * Make closeToast better and revert to admin password for matrix going back to 2.4 Signed-off-by: Derek Ho * Try experimental memory management Signed-off-by: Derek Ho * Try to fix closeToast Signed-off-by: Derek Ho * Fix for close toast Signed-off-by: Derek Ho * Find by warning Signed-off-by: Derek Ho * Fix by warning Signed-off-by: Derek Ho * Fix tenancy test from main Signed-off-by: Derek Ho * Fix auth test and be explicit about local datasource Signed-off-by: Derek Ho * Test fixes Signed-off-by: Derek Ho * Fix role flaky test Signed-off-by: Derek Ho * Nit Signed-off-by: Derek Ho * Makes github cypress run headless Signed-off-by: Darshit Chanpura * Enabled experimentalMemoryManagement for chromium based cypress tests Signed-off-by: Darshit Chanpura --------- Signed-off-by: Derek Ho Signed-off-by: Darshit Chanpura Co-authored-by: Darshit Chanpura --- ...ress-test-multidatasources-enabled-e2e.yml | 2 +- .github/workflows/integration-test.yml | 2 +- cypress.config.js | 1 + public/apps/configuration/app-router.tsx | 7 +- .../__snapshots__/audit-logging.test.tsx.snap | 2 +- .../internal-user-edit/internal-user-edit.tsx | 9 +- .../test/internal-user-edit.test.tsx | 10 +- .../permission-list/permission-list.tsx | 9 +- .../test/permission-list.test.tsx | 14 +- .../panels/role-edit/role-edit.tsx | 9 +- .../panels/role-edit/test/role-edit.test.tsx | 6 +- .../role-mapping/role-edit-mapped-user.tsx | 8 +- .../test/role-edit-mapped-user.test.tsx | 8 +- .../panels/role-view/role-view.tsx | 8 +- .../__snapshots__/role-view.test.tsx.snap | 8 +- .../panels/role-view/test/role-view.test.tsx | 20 +-- .../panels/tenant-list/manage_tab.tsx | 11 +- .../tenant-list/test/tenant-list.test.tsx | 24 +-- .../__snapshots__/get-started.test.tsx.snap | 4 +- .../__snapshots__/app-router.test.tsx.snap | 5 +- .../configuration/test/top-nav-menu.test.tsx | 1 + public/apps/configuration/top-nav-menu.tsx | 83 +++++------ .../utils/test/toast-utils.test.tsx | 19 ++- .../apps/configuration/utils/toast-utils.tsx | 16 +- public/utils/datasource-utils.ts | 14 +- public/utils/test/datasource-utils.test.ts | 55 ++++++- .../multi_datasources_enabled.spec.js | 141 +++++++++--------- 27 files changed, 307 insertions(+), 189 deletions(-) diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml index 92540eb6d..49bdf761a 100644 --- a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -123,4 +123,4 @@ jobs: 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"' + yarn_command: 'yarn cypress:run --browser chrome --headless --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 e35ae5363..34619622f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,7 +6,7 @@ env: TEST_BROWSER_HEADLESS: 1 CI: 1 PLUGIN_NAME: opensearch-security - OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! + OPENSEARCH_INITIAL_ADMIN_PASSWORD: admin jobs: tests: diff --git a/cypress.config.js b/cypress.config.js index e5a86728b..8aefc5440 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -32,4 +32,5 @@ module.exports = defineConfig({ adminUserName: 'admin', adminPassword: 'myStrongPassword123!', }, + experimentalMemoryManagement: true, }); diff --git a/public/apps/configuration/app-router.tsx b/public/apps/configuration/app-router.tsx index 491623ab3..8e23a3065 100644 --- a/public/apps/configuration/app-router.tsx +++ b/public/apps/configuration/app-router.tsx @@ -17,7 +17,7 @@ import { EuiBreadcrumb, EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eu import { flow, partial } from 'lodash'; import React, { createContext, useState } from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; -import { DataSourceOption } from '../../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; import { AppDependencies } from '../types'; import { AuditLogging } from './panels/audit-logging/audit-logging'; import { AuditLoggingEditSettings } from './panels/audit-logging/audit-logging-edit-settings'; @@ -41,6 +41,7 @@ import { Action, RouteItem, SubAction } from './types'; import { ResourceType } from '../../../common'; import { buildHashUrl, buildUrl } from './utils/url-builder'; import { CrossPageToast } from './cross-page-toast'; +import { getDataSourceFromUrl } from '../../utils/datasource-utils'; const LANDING_PAGE_URL = '/getstarted'; @@ -155,7 +156,9 @@ export const DataSourceContext = createContext(nul export function AppRouter(props: AppDependencies) { const setGlobalBreadcrumbs = flow(getBreadcrumbs, props.coreStart.chrome.setBreadcrumbs); - const [dataSource, setDataSource] = useState(LocalCluster); + const dataSourceFromUrl = getDataSourceFromUrl(); + + const [dataSource, setDataSource] = useState(dataSourceFromUrl); return ( diff --git a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap index 3e64fa6c9..9fdee791a 100644 --- a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap +++ b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap @@ -241,7 +241,7 @@ exports[`Audit logs render when AuditLoggingSettings.enabled is true 1`] = `
- (true); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { @@ -135,7 +136,13 @@ export function InternalUserEdit(props: InternalUserEditDeps) { setCrossPageToast(buildUrl(ResourceType.users), { id: 'updateUserSucceeded', color: 'success', - title: getSuccessToastMessage('User', props.action, userName), + title: `${getSuccessToastMessage( + 'User', + props.action, + userName, + dataSourceEnabled, + dataSource + )}`, }); // Redirect to user listing window.location.href = buildHashUrl(ResourceType.users); diff --git a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx index 8dcc630d8..c77b294bf 100644 --- a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx +++ b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx @@ -56,7 +56,7 @@ describe('Internal user edit', () => { sourceUserName={sampleUsername} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -77,7 +77,7 @@ describe('Internal user edit', () => { sourceUserName={sampleUsername} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -98,7 +98,7 @@ describe('Internal user edit', () => { sourceUserName={sampleUsername} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -118,7 +118,7 @@ describe('Internal user edit', () => { sourceUserName={sampleUsername} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -140,7 +140,7 @@ describe('Internal user edit', () => { sourceUserName={sampleUsername} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index ec90dd7d5..1983fd017 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -192,6 +192,7 @@ export function PermissionList(props: AppDependencies) { const [selection, setSelection] = React.useState([]); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; // Modal state @@ -305,7 +306,13 @@ export function PermissionList(props: AppDependencies) { fetchData(); addToast({ id: 'saveSucceeded', - title: getSuccessToastMessage('Action group', action, groupName), + title: `${getSuccessToastMessage( + 'Action group', + action, + groupName, + dataSourceEnabled, + dataSource + )}`, color: 'success', }); } catch (e) { diff --git a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx index 856a3e8fb..a15d2eace 100644 --- a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx +++ b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx @@ -109,7 +109,7 @@ describe('Permission list page ', () => { const component = shallow( @@ -123,7 +123,7 @@ describe('Permission list page ', () => { shallow( @@ -145,7 +145,7 @@ describe('Permission list page ', () => { shallow( @@ -159,7 +159,7 @@ describe('Permission list page ', () => { const component = shallow( @@ -186,7 +186,7 @@ describe('Permission list page ', () => { const component = shallow( @@ -203,7 +203,7 @@ describe('Permission list page ', () => { shallow( @@ -223,7 +223,7 @@ describe('Permission list page ', () => { const component = shallow( diff --git a/public/apps/configuration/panels/role-edit/role-edit.tsx b/public/apps/configuration/panels/role-edit/role-edit.tsx index ead2ee93d..6fdc3b281 100644 --- a/public/apps/configuration/panels/role-edit/role-edit.tsx +++ b/public/apps/configuration/panels/role-edit/role-edit.tsx @@ -89,6 +89,7 @@ export function RoleEdit(props: RoleEditDeps) { const [isFormValid, setIsFormValid] = useState(true); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { @@ -174,7 +175,13 @@ export function RoleEdit(props: RoleEditDeps) { setCrossPageToast(buildUrl(ResourceType.roles, Action.view, roleName), { id: 'updateRoleSucceeded', color: 'success', - title: getSuccessToastMessage('Role', props.action, roleName), + title: `${getSuccessToastMessage( + 'Role', + props.action, + roleName, + dataSourceEnabled, + dataSource + )}`, }); // Redirect to role view window.location.href = buildHashUrl(ResourceType.roles, Action.view, roleName); diff --git a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx index 81ac845be..4bc01ebe2 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx @@ -63,7 +63,7 @@ describe('Role edit', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -89,7 +89,7 @@ describe('Role edit', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -112,7 +112,7 @@ describe('Role edit', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx index a2ae92c60..38dc5354f 100644 --- a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx +++ b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx @@ -45,7 +45,7 @@ import { setCrossPageToast } from '../../utils/storage-utils'; import { ExternalLink } from '../../utils/display-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; +import { createDataSourceQuery, getClusterInfoIfEnabled } from '../../../../utils/datasource-utils'; interface RoleEditMappedUserProps extends BreadcrumbsPageDependencies { roleName: string; @@ -63,6 +63,7 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { const [userNames, setUserNames] = useState([]); const [hosts, setHosts] = React.useState([]); const [toasts, addToast, removeToast] = useToastState(); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; React.useEffect(() => { @@ -130,7 +131,10 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { { id: 'updateRoleMappingSucceeded', color: 'success', - title: 'Role "' + props.roleName + '" successfully updated.', + title: `Role "${props.roleName}" successfully updated ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}`, } ); window.location.href = buildHashUrl( diff --git a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx index 314ca803f..e78435817 100644 --- a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx +++ b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx @@ -56,7 +56,7 @@ describe('Role mapping edit', () => { roleName={sampleRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -81,7 +81,7 @@ describe('Role mapping edit', () => { roleName={sampleRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -102,7 +102,7 @@ describe('Role mapping edit', () => { roleName={sampleRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -135,7 +135,7 @@ describe('Role mapping edit', () => { roleName={sampleRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/role-view/role-view.tsx b/public/apps/configuration/panels/role-view/role-view.tsx index d82710486..9791f90bf 100644 --- a/public/apps/configuration/panels/role-view/role-view.tsx +++ b/public/apps/configuration/panels/role-view/role-view.tsx @@ -72,7 +72,7 @@ import { requestDeleteRoles } from '../../utils/role-list-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; +import { createDataSourceQuery, getClusterInfoIfEnabled } from '../../../../utils/datasource-utils'; interface RoleViewProps extends BreadcrumbsPageDependencies { roleName: string; @@ -111,6 +111,7 @@ export function RoleView(props: RoleViewProps) { const [toasts, addToast, removeToast] = useToastState(); const [isReserved, setIsReserved] = React.useState(false); const [loading, setLoading] = React.useState(false); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { dataSource, setDataSource } = useContext(DataSourceContext)!; const PERMISSIONS_TAB_INDEX = 0; @@ -378,7 +379,10 @@ export function RoleView(props: RoleViewProps) { setCrossPageToast(buildUrl(ResourceType.roles), { id: 'deleteRole', color: 'success', - title: props.roleName + ' deleted.', + title: `${props.roleName} deleted ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}`, }); window.location.href = buildHashUrl(ResourceType.roles); } catch (e) { diff --git a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap index 576e409b2..c5095452f 100644 --- a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap +++ b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Role view basic rendering when permission tab is selected 1`] = ` - - { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -123,7 +123,7 @@ describe('Role view', () => { prevAction={SubAction.mapuser} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -138,7 +138,7 @@ describe('Role view', () => { prevAction={SubAction.mapuser} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -159,7 +159,7 @@ describe('Role view', () => { prevAction={SubAction.mapuser} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -178,7 +178,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -207,7 +207,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -223,7 +223,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -249,7 +249,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -271,7 +271,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> @@ -291,7 +291,7 @@ describe('Role view', () => { prevAction="" buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - navigation={{} as any} + depsStart={{} as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/tenant-list/manage_tab.tsx b/public/apps/configuration/panels/tenant-list/manage_tab.tsx index bc36114b4..f6689ac95 100644 --- a/public/apps/configuration/panels/tenant-list/manage_tab.tsx +++ b/public/apps/configuration/panels/tenant-list/manage_tab.tsx @@ -68,7 +68,7 @@ import { useContextMenuState } from '../../utils/context-menu'; import { generateResourceName } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; import { TenantList } from './tenant-list'; -import { getBreadcrumbs } from '../../app-router'; +import { LocalCluster, getBreadcrumbs } from '../../app-router'; import { buildUrl } from '../../utils/url-builder'; import { CrossPageToast } from '../../cross-page-toast'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; @@ -90,6 +90,7 @@ export function ManageTab(props: AppDependencies) { const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(false); const [isPrivateTenantEnabled, setIsPrivateTenantEnabled] = useState(false); const [dashboardsDefaultTenant, setDashboardsDefaultTenant] = useState(''); + const dataSourceEnabled = !!props.depsStart.dataSource?.dataSourceEnabled; const { http } = props.coreStart; @@ -465,7 +466,13 @@ export function ManageTab(props: AppDependencies) { fetchData(); addToast({ id: 'saveSucceeded', - title: getSuccessToastMessage('Tenant', action, tenantName), + title: getSuccessToastMessage( + 'Tenant', + action, + tenantName, + dataSourceEnabled, + LocalCluster + ), color: 'success', }); } catch (e) { diff --git a/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx b/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx index b92dd8806..a00a417d8 100644 --- a/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx +++ b/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx @@ -98,7 +98,7 @@ describe('Tenant list', () => { const component = shallow( @@ -126,7 +126,7 @@ describe('Tenant list', () => { const component = shallow( @@ -145,7 +145,7 @@ describe('Tenant list', () => { shallow( @@ -175,7 +175,7 @@ describe('Tenant list', () => { shallow( @@ -195,7 +195,7 @@ describe('Tenant list', () => { shallow( @@ -220,7 +220,7 @@ describe('Tenant list', () => { shallow( @@ -239,7 +239,7 @@ describe('Tenant list', () => { const component = shallow( @@ -261,7 +261,7 @@ describe('Tenant list', () => { const component = shallow( @@ -299,7 +299,7 @@ describe('Tenant list', () => { const component = shallow( @@ -315,7 +315,7 @@ describe('Tenant list', () => { const component = shallow( @@ -335,7 +335,7 @@ describe('Tenant list', () => { const component = shallow( @@ -364,7 +364,7 @@ describe('Tenant list', () => { component = shallow( 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 f615b622b..0b2126950 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,7 +5,7 @@ exports[`Get started (landing page) renders when backend configuration is disabl
- - { depsStart={securityPluginStartDepsMock} dataSourcePickerReadOnly={false} dataSourceManagement={dataSourceManagementMock} + selectedDataSource={{}} params={{}} /> ); diff --git a/public/apps/configuration/top-nav-menu.tsx b/public/apps/configuration/top-nav-menu.tsx index c582700e5..669a6d805 100644 --- a/public/apps/configuration/top-nav-menu.tsx +++ b/public/apps/configuration/top-nav-menu.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { DataSourceSelectableConfig } from 'src/plugins/data_source_management/public'; import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; import { AppDependencies } from '../types'; +import { setDataSourceInUrl } from '../../utils/datasource-utils'; export interface TopNavMenuProps extends AppDependencies { dataSourcePickerReadOnly: boolean; @@ -24,51 +25,43 @@ export interface TopNavMenuProps extends AppDependencies { selectedDataSource: DataSourceOption; } -const compatibleVersion = new Set([ - '2.1', - '2.2', - '2.3', - '2.4', - '2.5', - '2.6', - '2.7', - '2.8', - '2.9', - '2.10', - '2.11', - '2.12', -]); +export const SecurityPluginTopNavMenu = React.memo( + (props: TopNavMenuProps) => { + const { + coreStart, + depsStart, + params, + dataSourceManagement, + setDataSource, + selectedDataSource, + dataSourcePickerReadOnly, + } = props; + const { setHeaderActionMenu } = params; + const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu(); -export const SecurityPluginTopNavMenu = (props: TopNavMenuProps) => { - const { - coreStart, - depsStart, - params, - dataSourceManagement, - setDataSource, - selectedDataSource, - dataSourcePickerReadOnly, - } = props; - const { setHeaderActionMenu } = params; - const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu(); + const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; - const dataSourceEnabled = !!depsStart.dataSource?.dataSourceEnabled; + const wrapSetDataSourceWithUpdateUrl = (dataSources: DataSourceOption[]) => { + setDataSourceInUrl(dataSources[0]); + setDataSource(dataSources[0]); + }; - return dataSourceEnabled ? ( - compatibleVersion.has(ds.attributes.version), - onSelectedDataSources: (dataSources) => { - // single select for now - setDataSource(dataSources[0]); - }, - fullWidth: true, - }} - /> - ) : null; -}; + return dataSourceEnabled ? ( + + ) : null; + }, + (prevProps, newProps) => + prevProps.selectedDataSource.id === newProps.selectedDataSource.id && + prevProps.dataSourcePickerReadOnly === newProps.dataSourcePickerReadOnly +); diff --git a/public/apps/configuration/utils/test/toast-utils.test.tsx b/public/apps/configuration/utils/test/toast-utils.test.tsx index 921751da5..82b6b8ad9 100644 --- a/public/apps/configuration/utils/test/toast-utils.test.tsx +++ b/public/apps/configuration/utils/test/toast-utils.test.tsx @@ -101,21 +101,30 @@ describe('Toast utils', () => { describe('getSuccessToastMessage', () => { it('should return successful create message', () => { - const result = getSuccessToastMessage('User', 'create', 'user1'); + const result = getSuccessToastMessage('User', 'create', 'user1', false, { id: '' }); - expect(result).toEqual('User "user1" successfully created'); + expect(result).toEqual('User "user1" successfully created '); }); it('should return successful update message', () => { - const result = getSuccessToastMessage('Role', 'edit', 'role1'); + const result = getSuccessToastMessage('Role', 'edit', 'role1', false, { id: '' }); - expect(result).toEqual('Role "role1" successfully updated'); + expect(result).toEqual('Role "role1" successfully updated '); }); it('should return empty message for unknown action', () => { - const result = getSuccessToastMessage('User', '', 'user1'); + const result = getSuccessToastMessage('User', '', 'user1', false, { id: '' }); expect(result).toEqual(''); }); + + it('should return successful update message for remote cluster', () => { + const result = getSuccessToastMessage('Role', 'edit', 'role1', true, { + id: '', + label: 'blah', + }); + + expect(result).toEqual('Role "role1" successfully updated for blah'); + }); }); }); diff --git a/public/apps/configuration/utils/toast-utils.tsx b/public/apps/configuration/utils/toast-utils.tsx index db6fea635..450c01be7 100644 --- a/public/apps/configuration/utils/toast-utils.tsx +++ b/public/apps/configuration/utils/toast-utils.tsx @@ -15,6 +15,8 @@ import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { useState, useCallback } from 'react'; +import { getClusterInfoIfEnabled } from '../../../utils/datasource-utils'; +import { DataSourceOption } from '../../../../../../src/plugins/data_source_management/public/components/data_source_menu/types'; export function createErrorToast(id: string, title: string, text: string): Toast { return { @@ -66,14 +68,22 @@ export function useToastState(): [Toast[], (toAdd: Toast) => void, (toDelete: To export function getSuccessToastMessage( resourceType: string, action: string, - userName: string + userName: string, + dataSourceEnabled: boolean, + dataSource: DataSourceOption ): string { switch (action) { case 'create': case 'duplicate': - return `${resourceType} "${userName}" successfully created`; + return `${resourceType} "${userName}" successfully created ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}`; case 'edit': - return `${resourceType} "${userName}" successfully updated`; + return `${resourceType} "${userName}" successfully updated ${getClusterInfoIfEnabled( + dataSourceEnabled, + dataSource + )}`; default: return ''; } diff --git a/public/utils/datasource-utils.ts b/public/utils/datasource-utils.ts index 79de76a2f..59db29aac 100644 --- a/public/utils/datasource-utils.ts +++ b/public/utils/datasource-utils.ts @@ -13,15 +13,27 @@ * permissions and limitations under the License. */ -import { DataSourceOption } from '../../../../src/plugins/data_source_management/public/components/data_source_selector/data_source_selector'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; export function createDataSourceQuery(dataSourceId: string) { return { dataSourceId }; } +const DATASOURCEURLKEY = 'dataSource'; + export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: DataSourceOption) { if (dataSourceEnabled) { return `for ${cluster.label || 'Local cluster'}`; } return ''; } + +export function getDataSourceFromUrl(): DataSourceOption { + return JSON.parse(new URLSearchParams(window.location.search).get(DATASOURCEURLKEY) || '{}'); +} + +export function setDataSourceInUrl(dataSource: DataSourceOption) { + const url = new URL(window.location.href); + url.searchParams.set(DATASOURCEURLKEY, JSON.stringify(dataSource)); + window.history.replaceState({}, '', url.toString()); +} diff --git a/public/utils/test/datasource-utils.test.ts b/public/utils/test/datasource-utils.test.ts index a34ddd951..9db4af61b 100644 --- a/public/utils/test/datasource-utils.test.ts +++ b/public/utils/test/datasource-utils.test.ts @@ -13,7 +13,12 @@ * permissions and limitations under the License. */ -import { createDataSourceQuery, getClusterInfoIfEnabled } from '../datasource-utils'; +import { + createDataSourceQuery, + getClusterInfoIfEnabled, + getDataSourceFromUrl, + setDataSourceInUrl, +} from '../datasource-utils'; describe('Tests datasource utils', () => { it('Tests the GetClusterDescription helper function', () => { @@ -25,4 +30,52 @@ describe('Tests datasource utils', () => { it('Tests the create DataSource query helper function', () => { expect(createDataSourceQuery('test')).toStrictEqual({ dataSourceId: 'test' }); }); + + it('Tests getting the datasource from the url', () => { + const mockSearchNoDataSourceId = '?foo=bar&baz=qux'; + Object.defineProperty(window, 'location', { + value: { search: mockSearchNoDataSourceId }, + writable: true, + }); + expect(getDataSourceFromUrl()).toEqual({}); + const mockSearchDataSourceIdNotfirst = + '?foo=bar&baz=qux&dataSource=%7B"id"%3A"94ffa650-f11a-11ee-a585-793f7b098e1a"%2C"label"%3A"9202"%7D'; + Object.defineProperty(window, 'location', { + value: { search: mockSearchDataSourceIdNotfirst }, + writable: true, + }); + expect(getDataSourceFromUrl()).toEqual({ + id: '94ffa650-f11a-11ee-a585-793f7b098e1a', + label: '9202', + }); + const mockSearchDataSourceIdFirst = + '?dataSource=%7B"id"%3A"94ffa650-f11a-11ee-a585-793f7b098e1a"%2C"label"%3A"9202"%7D'; + Object.defineProperty(window, 'location', { + value: { search: mockSearchDataSourceIdFirst }, + writable: true, + }); + expect(getDataSourceFromUrl()).toEqual({ + id: '94ffa650-f11a-11ee-a585-793f7b098e1a', + label: '9202', + }); + }); + + it('Tests setting the datasource in the url', () => { + const replaceState = jest.fn(); + const mockUrl = 'http://localhost:5601/app/security-dashboards-plugin#/auth'; + Object.defineProperty(window, 'location', { + value: { href: mockUrl }, + writable: true, + }); + Object.defineProperty(window, 'history', { + value: { replaceState }, + writable: true, + }); + setDataSourceInUrl({ id: '', label: 'Local cluster' }); + expect(replaceState).toBeCalledWith( + {}, + '', + 'http://localhost:5601/app/security-dashboards-plugin?dataSource=%7B%22id%22%3A%22%22%2C%22label%22%3A%22Local+cluster%22%7D#/auth' + ); + }); }); diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 663c73e02..4b18d401b 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ +const externalTitle = '9202'; const createDataSource = () => { - cy.request({ + return cy.request({ method: 'POST', url: `${Cypress.config('baseUrl')}/api/saved_objects/data-source`, headers: { @@ -22,7 +23,7 @@ const createDataSource = () => { }, body: { attributes: { - title: '9202', + title: externalTitle, endpoint: 'https://localhost:9202', auth: { type: 'username_password', @@ -38,11 +39,10 @@ const createDataSource = () => { const closeToast = () => { // remove browser incompatibiltiy toast causing flakyness (cause it has higher z-index than Create button making it invisible) - cy.get('body').then((body) => { - if (body.find('[data-test-subj="toastCloseButton"]').length > 0) { - cy.get('[data-test-subj="toastCloseButton"]').click(); - } - }); + cy.get('[class="euiToast euiToast--warning euiGlobalToastListItem"]') + .find('[data-test-subj="toastCloseButton"]') + .first() + .click(); }; const deleteAllDataSources = () => { @@ -67,12 +67,28 @@ const deleteAllDataSources = () => { }); }; +const createUrlParam = (label, id) => { + const dataSourceObj = { label, id }; + + return `?dataSource=${JSON.stringify(dataSourceObj)}`; +}; + +let externalDataSourceId; +let externalDataSourceUrl; +let localDataSourceUrl; + describe('Multi-datasources enabled', () => { before(() => { deleteAllDataSources(); localStorage.setItem('opendistro::security::tenant::saved', '""'); localStorage.setItem('home:newThemeModal:show', 'false'); - createDataSource(); + createDataSource().then((resp) => { + if (resp && resp.body) { + externalDataSourceId = resp.body.id; + } + externalDataSourceUrl = createUrlParam(externalTitle, externalDataSourceId); + localDataSourceUrl = createUrlParam('Local cluster', ''); + }); }); after(() => { @@ -81,41 +97,37 @@ describe('Multi-datasources enabled', () => { }); it('Checks Get Started Tab', () => { - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/getstarted'); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/getstarted` + ); + closeToast(); // 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.get('[title="9202"]').click(); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/getstarted` + ); + closeToast(); cy.get('[data-test-subj="purge-cache"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); - // Data source persisted across tabs - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); }); it('Checks Auth Tab', () => { - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/auth`); + closeToast(); // Local cluster auth cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); // Remote cluster auth - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="9202"]').click(); - cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auth`); + closeToast(); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); }); it('Checks Users Tab', () => { - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); - // Create an internal user in the remote cluster - cy.contains('h3', 'Internal users'); - cy.contains('a', 'admin'); - - closeToast(); - // select remote data source - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="9202"]').click(); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/users`); + closeToast(); // create a user on remote data source cy.get('[data-test-subj="create-user"]').click(); @@ -129,11 +141,12 @@ describe('Multi-datasources enabled', () => { 'contain', '9202' ); + cy.get('[data-test-subj="tableHeaderCell_username_0"]').click(); cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); // Internal user doesn't exist on local cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="Local cluster"]').click(); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/users`); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', 'Local cluster' @@ -142,15 +155,11 @@ describe('Multi-datasources enabled', () => { }); it('Checks Permissions Tab', () => { - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/permissions'); - // Create a permission in the remote cluster - cy.contains('h3', 'Permissions'); - - closeToast(); - // Select remote cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="9202"]').click(); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/permissions` + ); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', '9202' @@ -165,7 +174,6 @@ describe('Multi-datasources enabled', () => { .should('have.value', 'test_permission_ag'); cy.get('[data-test-subj="comboBoxInput"]').focus().type('some_permission'); cy.get('[id="submit"]').click(); - closeToast(); // Permission exists on the remote data source cy.get('[data-text="Customization"]').click(); @@ -173,8 +181,10 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('exist'); // Permission doesn't exist on local cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="Local cluster"]').click(); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/permissions` + ); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', 'Local cluster' @@ -184,8 +194,9 @@ describe('Multi-datasources enabled', () => { it('Checks Tenancy Tab', () => { // Datasource is locked to local cluster for tenancy tab - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/tenants'); - cy.contains('h1', 'Multi-tenancy'); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/tenants`); + closeToast(); + cy.contains('h1', 'Dashboards multi-tenancy'); cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( 'contain', 'Local cluster' @@ -195,7 +206,10 @@ describe('Multi-datasources enabled', () => { it('Checks Service Accounts Tab', () => { // Datasource is locked to local cluster for service accounts tab - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/serviceAccounts'); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/serviceAccounts` + ); + closeToast(); cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( 'contain', 'Local cluster' @@ -204,12 +218,11 @@ describe('Multi-datasources enabled', () => { }); it('Checks Audit Logs Tab', () => { - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auditLogging'); - cy.get('[data-test-subj="general-settings"]').should('exist'); - // Select remote cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="9202"]').click(); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auditLogging` + ); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', '9202' @@ -218,15 +231,16 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="general-settings-configure"]').click(); cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('contain', '9202'); - closeToast(); cy.get('[data-test-subj="comboBoxInput"]').last().type('blah'); cy.get('[data-test-subj="save"]').click(); cy.get('[data-test-subj="general-settings"]').should('contain', 'blah'); // Select local cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="Local cluster"]').click(); + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/auditLogging` + ); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', 'Local cluster' @@ -237,43 +251,32 @@ describe('Multi-datasources enabled', () => { it('Checks Roles Tab', () => { Cypress.on('uncaught:exception', (err) => !err.message.includes('ResizeObserver')); - - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/roles'); - cy.contains('h3', 'Roles'); - - closeToast(); - // select remote data source - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="9202"]').click(); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles`); + closeToast(); // create a role on remote data source cy.get('[data-test-subj="create-role"]').click(); cy.contains('h1', 'Create Role'); cy.get('[data-test-subj="name-text"]').focus().type('9202-role'); cy.get('[data-test-subj="comboBoxToggleListButton"]').first().click(); - cy.get('button[title="manage_snapshots"]').should('be.visible'); - cy.get('button[title="manage_snapshots"]').click({ force: true }); - - cy.get('[data-test-subj="comboBoxInput"]').first().should('contain', 'manage_snapshots'); cy.get('[data-test-subj="create-or-update-role"]').click(); cy.get('.euiToastHeader__title').should('contain', 'Role "9202-role" successfully created'); - closeToast(); // role exists on the remote - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/roles'); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles`); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', '9202' ); - cy.get('[data-text="Customization"]').click(); - cy.get('[data-test-subj="filter-custom"]').click(); + cy.get('[data-test-subj="tableHeaderCell_roleName_0"]').click(); cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('exist'); // Role doesn't exist on local cluster - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.get('[title="Local cluster"]').click(); + cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/roles`); + closeToast(); cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( 'contain', 'Local cluster' From 5f0f592425e80f624779f73ae5ad967ec8214c27 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Mon, 15 Apr 2024 14:25:43 -0400 Subject: [PATCH 11/22] Revert to "admin" password for older versions Signed-off-by: Derek Ho --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index e35ae5363..34619622f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,7 +6,7 @@ env: TEST_BROWSER_HEADLESS: 1 CI: 1 PLUGIN_NAME: opensearch-security - OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! + OPENSEARCH_INITIAL_ADMIN_PASSWORD: admin jobs: tests: From 9a7ed1eb129e74c41636018effc52c1b3f0cd10d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:36:50 -0400 Subject: [PATCH 12/22] [MDS] Fixes flaky cypress tests for data-source picker (#1898) * Fix some state issues and try to use electron browser Signed-off-by: Derek Ho * Try modifying numtestskeptinmemory Signed-off-by: Derek Ho * Remove closetoast Signed-off-by: Derek Ho * Fix flaky cypress test Signed-off-by: Derek Ho * Run 10x Signed-off-by: Derek Ho * Fix test Signed-off-by: Derek Ho * Fix permissions test Signed-off-by: Derek Ho * Move management to correct section Signed-off-by: Derek Ho * Push up fixes for audit logs and roles Signed-off-by: Derek Ho * Remove local cluster checks to cut length and increase stability Signed-off-by: Derek Ho * Improves before and after handling Signed-off-by: Darshit Chanpura * Temporarily disable permissions and roles tests Signed-off-by: Darshit Chanpura * adds delete datasource call to beforeEach Signed-off-by: Darshit Chanpura * Unskip permissions test Signed-off-by: Darshit Chanpura * Absorb changes from core Signed-off-by: Darshit Chanpura * Fixes tests Signed-off-by: Darshit Chanpura * Skips legacy browser warning Signed-off-by: Darshit Chanpura * Refactor audit logs tests Signed-off-by: Darshit Chanpura * Force page reload post creation Signed-off-by: Darshit Chanpura * Unskip auth and roles, and skip audit logs Signed-off-by: Darshit Chanpura * Skip roles tab Signed-off-by: Darshit Chanpura * Changes data source url Signed-off-by: Darshit Chanpura * Fix lint issues Signed-off-by: Darshit Chanpura * Chrome Signed-off-by: Derek Ho * Skip users and permissions tab Signed-off-by: Derek Ho * Revert cypress settings Signed-off-by: Derek Ho * Modify test to make cypress request instead of UI Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Add fix for get started and rewrite users test Signed-off-by: Derek Ho * use tostring to be more consistent with the actual behavior Signed-off-by: Derek Ho * Try to fix flaky tests Signed-off-by: Derek Ho * Revert to beforeeach and aftereach Signed-off-by: Derek Ho * Fix test Signed-off-by: Derek Ho * Fix lint Signed-off-by: Derek Ho * Fix roles and enable Signed-off-by: Derek Ho * Adds retry mechanism to cypress tests Signed-off-by: Darshit Chanpura * Increases retries to 5 Signed-off-by: Darshit Chanpura * Reverts CI runs to 1 Signed-off-by: Darshit Chanpura --------- Signed-off-by: Derek Ho Signed-off-by: Darshit Chanpura Co-authored-by: Derek Ho --- .github/actions/run-cypress-tests/action.yml | 15 +- cypress.config.js | 1 - .../role-edit/index-permission-panel.tsx | 10 +- .../panels/role-edit/tenant-panel.tsx | 11 +- .../test/index-permission-panel.test.tsx | 3 +- .../role-edit/test/tenant-panel.test.tsx | 3 +- public/utils/datasource-utils.ts | 4 +- .../multi_datasources_enabled.spec.js | 284 ++++++++---------- 8 files changed, 150 insertions(+), 181 deletions(-) diff --git a/.github/actions/run-cypress-tests/action.yml b/.github/actions/run-cypress-tests/action.yml index 74e001ff7..eae07eac8 100644 --- a/.github/actions/run-cypress-tests/action.yml +++ b/.github/actions/run-cypress-tests/action.yml @@ -63,7 +63,7 @@ runs: if: ${{ runner.os == 'Linux' }} run: | cd ./OpenSearch-Dashboards - nohup yarn start --no-base-path --no-watch | tee dashboard.log & + nohup yarn start --no-base-path --no-watch --csp.warnLegacyBrowsers=false | tee dashboard.log & shell: bash # Check if OSD is ready with a max timeout of 600 seconds @@ -85,8 +85,11 @@ runs: done shell: bash - - name: Run Cypress - run : | - yarn add cypress --save-dev - eval ${{ inputs.yarn_command }} - shell: bash + - name: Run Cypress Tests with retry + uses: Wandalen/wretry.action@v3.3.0 + with: + attempt_limit: 5 + attempt_delay: 2000 + command: | + yarn add cypress --save-dev + eval ${{ inputs.yarn_command }} diff --git a/cypress.config.js b/cypress.config.js index 8aefc5440..e5a86728b 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -32,5 +32,4 @@ module.exports = defineConfig({ adminUserName: 'admin', adminPassword: 'myStrongPassword123!', }, - experimentalMemoryManagement: true, }); diff --git a/public/apps/configuration/panels/role-edit/index-permission-panel.tsx b/public/apps/configuration/panels/role-edit/index-permission-panel.tsx index e52cb9128..a8a4284b6 100644 --- a/public/apps/configuration/panels/role-edit/index-permission-panel.tsx +++ b/public/apps/configuration/panels/role-edit/index-permission-panel.tsx @@ -23,7 +23,7 @@ import { EuiSuperSelect, EuiTextArea, } from '@elastic/eui'; -import React, { Dispatch, Fragment, SetStateAction } from 'react'; +import React, { Dispatch, Fragment, SetStateAction, useEffect } from 'react'; import { isEmpty } from 'lodash'; import { RoleIndexPermission } from '../../types'; import { ResourceType } from '../../../../../common'; @@ -320,9 +320,11 @@ export function IndexPermissionPanel(props: { }) { const { state, optionUniverse, setState } = props; // Show one empty row if there is no data. - if (isEmpty(state)) { - setState([getEmptyIndexPermission()]); - } + useEffect(() => { + if (isEmpty(state)) { + setState([getEmptyIndexPermission()]); + } + }, [state, setState]); return ( { + if (isEmpty(state)) { + setState([getEmptyTenantPermission()]); + } + }, [state, setState]); return ( { const state: RoleIndexPermissionStateClass[] = []; const optionUniverse: ComboBoxOptions = []; - shallow(); + render(); expect(setState).toHaveBeenCalledTimes(1); }); diff --git a/public/apps/configuration/panels/role-edit/test/tenant-panel.test.tsx b/public/apps/configuration/panels/role-edit/test/tenant-panel.test.tsx index 6fd5adcdc..d7ad8f36c 100644 --- a/public/apps/configuration/panels/role-edit/test/tenant-panel.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/tenant-panel.test.tsx @@ -24,6 +24,7 @@ import { import { shallow } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiButton, EuiSuperSelect } from '@elastic/eui'; +import { render } from '@testing-library/react'; jest.mock('../../../utils/array-state-utils'); // eslint-disable-next-line @@ -76,7 +77,7 @@ describe('Role edit - tenant panel', () => { const setState = jest.fn(); it('render an empty row if data is empty', () => { - shallow(); + render(); expect(setState).toHaveBeenCalledWith([ { diff --git a/public/utils/datasource-utils.ts b/public/utils/datasource-utils.ts index 59db29aac..f5681f0ec 100644 --- a/public/utils/datasource-utils.ts +++ b/public/utils/datasource-utils.ts @@ -29,7 +29,9 @@ export function getClusterInfoIfEnabled(dataSourceEnabled: boolean, cluster: Dat } export function getDataSourceFromUrl(): DataSourceOption { - return JSON.parse(new URLSearchParams(window.location.search).get(DATASOURCEURLKEY) || '{}'); + const urlParams = new URLSearchParams(window.location.search); + const dataSourceParam = (urlParams && urlParams.get(DATASOURCEURLKEY)) || '{}'; + return JSON.parse(dataSourceParam); } export function setDataSourceInUrl(dataSource: DataSourceOption) { diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 4b18d401b..2f30d5d3c 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -37,14 +37,6 @@ const createDataSource = () => { }); }; -const closeToast = () => { - // remove browser incompatibiltiy toast causing flakyness (cause it has higher z-index than Create button making it invisible) - cy.get('[class="euiToast euiToast--warning euiGlobalToastListItem"]') - .find('[data-test-subj="toastCloseButton"]') - .first() - .click(); -}; - const deleteAllDataSources = () => { cy.request( 'GET', @@ -69,8 +61,7 @@ const deleteAllDataSources = () => { const createUrlParam = (label, id) => { const dataSourceObj = { label, id }; - - return `?dataSource=${JSON.stringify(dataSourceObj)}`; + return `?dataSource=${JSON.stringify(dataSourceObj).toString()}`; }; let externalDataSourceId; @@ -78,8 +69,7 @@ let externalDataSourceUrl; let localDataSourceUrl; describe('Multi-datasources enabled', () => { - before(() => { - deleteAllDataSources(); + beforeEach(() => { localStorage.setItem('opendistro::security::tenant::saved', '""'); localStorage.setItem('home:newThemeModal:show', 'false'); createDataSource().then((resp) => { @@ -91,117 +81,86 @@ describe('Multi-datasources enabled', () => { }); }); - after(() => { + afterEach(() => { + cy.clearCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); deleteAllDataSources(); - cy.clearLocalStorage(); }); it('Checks Get Started Tab', () => { - cy.visit( - `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/getstarted` - ); - closeToast(); - // 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.visit( `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/getstarted` ); - closeToast(); + + cy.contains('h1', 'Get started'); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + cy.get('[data-test-subj="purge-cache"]').click(); - cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); + cy.get('[class="euiToast euiToast--success euiGlobalToastListItem"]') + .get('.euiToastHeader__title') + .should('contain', 'successful for 9202'); }); it('Checks Auth Tab', () => { - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/auth`); - closeToast(); - // Local cluster auth - cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); - // Remote cluster auth cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auth`); - closeToast(); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); }); it('Checks Users Tab', () => { - // select remote data source - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/users`); - closeToast(); - - // create a user on remote data source - cy.get('[data-test-subj="create-user"]').click(); - cy.get('[data-test-subj="name-text"]').focus().type('9202-user'); - cy.get('[data-test-subj="password"]').focus().type('myStrongPassword123!'); - cy.get('[data-test-subj="re-enter-password"]').focus().type('myStrongPassword123!'); - cy.get('[data-test-subj="submit-save-user"]').click(); - - // Internal user exists on the remote - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); - cy.get('[data-test-subj="tableHeaderCell_username_0"]').click(); - cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); + cy.request({ + method: 'POST', + url: `http://localhost:5601/api/v1/configuration/internalusers/9202-user?dataSourceId=${externalDataSourceId}`, + headers: { + 'osd-xsrf': true, + }, + body: { + backend_roles: [''], + attributes: {}, + password: 'myStrongPassword123!', + }, + }).then(() => { + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/users` + ); - // Internal user doesn't exist on local cluster - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/users`); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('not.exist'); + cy.get('[data-test-subj="tableHeaderCell_username_0"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); + }); }); it('Checks Permissions Tab', () => { - // Select remote cluster - cy.visit( - `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/permissions` - ); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); - - // Create an action group - cy.get('[id="Create action group"]').click(); - cy.get('[id="create-from-blank"]').click(); - cy.get('[data-test-subj="name-text"]') - .focus() - .type('test_permission_ag', { force: true }) - .should('have.value', 'test_permission_ag'); - cy.get('[data-test-subj="comboBoxInput"]').focus().type('some_permission'); - cy.get('[id="submit"]').click(); - - // Permission exists on the remote data source - cy.get('[data-text="Customization"]').click(); - cy.get('[data-test-subj="filter-custom"]').click(); - cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('exist'); - - // Permission doesn't exist on local cluster - cy.visit( - `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/permissions` - ); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('not.exist'); + cy.request({ + method: 'POST', + url: `http://localhost:5601/api/v1/configuration/actiongroups/9202-permission?dataSourceId=${externalDataSourceId}`, + headers: { + 'osd-xsrf': true, + }, + body: { + allowed_actions: [], + }, + }).then(() => { + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/permissions` + ); + + // Permission exists on the remote data source + cy.get('[data-test-subj="tableHeaderCell_name_0"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-9202-permission"]').should('exist'); + }); }); it('Checks Tenancy Tab', () => { // Datasource is locked to local cluster for tenancy tab cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/tenants`); - closeToast(); + cy.contains('h1', 'Dashboards multi-tenancy'); - cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('be.disabled'); + cy.get('[data-test-subj="dataSourceViewButton"]').should('contain', 'Local cluster'); }); it('Checks Service Accounts Tab', () => { @@ -209,78 +168,77 @@ describe('Multi-datasources enabled', () => { cy.visit( `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/serviceAccounts` ); - closeToast(); - cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('be.disabled'); + + cy.get('[data-test-subj="dataSourceViewButton"]').should('contain', 'Local cluster'); }); it('Checks Audit Logs Tab', () => { - // Select remote cluster - cy.visit( - `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auditLogging` - ); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); - - cy.get('[data-test-subj="general-settings-configure"]').click(); - cy.get('[data-test-subj="dataSourceViewContextMenuHeaderLink"]').should('contain', '9202'); - - cy.get('[data-test-subj="comboBoxInput"]').last().type('blah'); - cy.get('[data-test-subj="save"]').click(); - - cy.get('[data-test-subj="general-settings"]').should('contain', 'blah'); - - // Select local cluster - cy.visit( - `http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/auditLogging` - ); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - - cy.get('[data-test-subj="general-settings"]').should('not.contain', 'blah'); + cy.request({ + method: 'POST', + url: `http://localhost:5601/api/v1/configuration/audit/config?dataSourceId=${externalDataSourceId}`, + headers: { + 'osd-xsrf': true, + }, + body: { + compliance: { + enabled: true, + write_log_diffs: false, + read_watched_fields: {}, + read_ignore_users: ['kibanaserver'], + write_watched_indices: [], + write_ignore_users: ['kibanaserver'], + internal_config: false, + read_metadata_only: false, + write_metadata_only: false, + external_config: false, + }, + enabled: false, + audit: { + ignore_users: ['kibanaserver'], + ignore_requests: [], + disabled_rest_categories: ['AUTHENTICATED', 'GRANTED_PRIVILEGES'], + disabled_transport_categories: ['AUTHENTICATED', 'GRANTED_PRIVILEGES'], + log_request_body: true, + resolve_indices: true, + resolve_bulk_requests: false, + enable_transport: true, + enable_rest: true, + exclude_sensitive_headers: true, + }, + }, + }).then(() => { + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auditLogging` + ); + cy.get('[class="euiSwitch__label"]').should('contain', 'Disabled'); + }); }); it('Checks Roles Tab', () => { - Cypress.on('uncaught:exception', (err) => !err.message.includes('ResizeObserver')); - // select remote data source - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles`); - closeToast(); - - // create a role on remote data source - cy.get('[data-test-subj="create-role"]').click(); - cy.contains('h1', 'Create Role'); - cy.get('[data-test-subj="name-text"]').focus().type('9202-role'); - cy.get('[data-test-subj="comboBoxToggleListButton"]').first().click(); - cy.get('[data-test-subj="create-or-update-role"]').click(); - - cy.get('.euiToastHeader__title').should('contain', 'Role "9202-role" successfully created'); - - // role exists on the remote - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles`); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); - cy.get('[data-test-subj="tableHeaderCell_roleName_0"]').click(); - cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('exist'); - - // Role doesn't exist on local cluster - cy.visit(`http://localhost:5601/app/security-dashboards-plugin${localDataSourceUrl}#/roles`); - closeToast(); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - 'Local cluster' - ); - cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('not.exist'); + cy.request({ + method: 'POST', + url: `http://localhost:5601/api/v1/configuration/roles/9202-role?dataSourceId=${externalDataSourceId}`, + headers: { + 'osd-xsrf': true, + }, + body: { + cluster_permissions: [], + index_permissions: [], + tenant_permissions: [], + }, + }).then(() => { + cy.visit( + `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles` + ); + + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + cy.get('[data-test-subj="tableHeaderCell_roleName_0"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('exist'); + + // role exists on the remote + }); }); }); From 9cce39996fc5efa906793400a383c239529fb0cf Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 23 Apr 2024 15:13:30 -0400 Subject: [PATCH 13/22] Address PR feedback Signed-off-by: Derek Ho --- .github/actions/download-plugin/action.yml | 3 ++- ...ress-test-multidatasources-enabled-e2e.yml | 19 +------------------ .github/workflows/integration-test.yml | 12 ++++++------ .../audit-logging-edit-settings.tsx | 10 ++++------ .../apps/configuration/panels/get-started.tsx | 14 ++++---------- .../permission-list/permission-list.tsx | 6 +----- .../role-mapping/role-edit-mapped-user.tsx | 4 ++-- .../panels/role-view/role-view.tsx | 7 ++----- .../apps/configuration/utils/toast-utils.tsx | 6 +++--- public/utils/datasource-utils.ts | 2 +- public/utils/test/datasource-utils.test.ts | 8 ++++---- server/plugin.ts | 2 -- server/routes/index.ts | 5 +++++ .../multi_datasources_enabled.spec.js | 2 +- 14 files changed, 36 insertions(+), 64 deletions(-) diff --git a/.github/actions/download-plugin/action.yml b/.github/actions/download-plugin/action.yml index 6be121f66..cedcc87f8 100644 --- a/.github/actions/download-plugin/action.yml +++ b/.github/actions/download-plugin/action.yml @@ -27,4 +27,5 @@ runs: -Dartifact=org.opensearch.plugin:${{ inputs.plugin-name }}:${{ inputs.plugin-version }}-SNAPSHOT:zip \ -Dtransitive=false \ -Ddest=${{ inputs.download-location }}.zip - shell: bash \ No newline at end of file + shell: bash + \ No newline at end of file diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml index 49bdf761a..4ae9b05e3 100644 --- a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -56,23 +56,6 @@ jobs: challenge: false authentication_backend: type: intern - saml_auth_domain: - http_enabled: true - transport_enabled: false - order: 1 - http_authenticator: - type: saml - challenge: true - config: - idp: - entity_id: urn:example:idp - metadata_url: http://localhost:7000/metadata - sp: - entity_id: https://localhost:9200 - kibana_url: http://localhost:5601 - exchange_key: 6aff3042-1327-4f3d-82f0-40a157ac4464 - authentication_backend: - type: noop EOT - name: Download security plugin and create setup scripts @@ -96,7 +79,7 @@ jobs: - name: Check OpenSearch is running # Verify that the server is operational run: | - curl https://localhost:9202/_cat/plugins -v -u admin:myStrongPassword123! -k + curl https://localhost:9202/_cat/plugins -v -u admin:${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} -k shell: bash # Configure the Dashboard for multi datasources diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 34619622f..78a5b6d50 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest , windows-latest ] - os_bwc_version: [2.4.0, 2.5.0, 2.6.0, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 2.11.0, 2.12.0, 3.0.0] + datasource_version: [1.3.9, 2.4.0, 2.5.0, 2.13.0, 3.0.0] runs-on: ${{ matrix.os }} steps: @@ -38,10 +38,10 @@ jobs: - name: Download security plugin and create setup scripts for remote cluster uses: ./.github/actions/download-plugin with: - opensearch-version: ${{ matrix.os_bwc_version }} + opensearch-version: ${{ matrix.datasource_version }} plugin-name: ${{ env.PLUGIN_NAME }} - download-location: ${{env.PLUGIN_NAME}}-${{matrix.os_bwc_version}} - plugin-version: ${{matrix.os_bwc_version}}.0 + download-location: ${{env.PLUGIN_NAME}}-${{matrix.datasource_version}} + plugin-version: ${{matrix.datasource_version}}.0 - name: Download security plugin and create setup scripts for local cluster uses: ./.github/actions/download-plugin @@ -54,8 +54,8 @@ jobs: - name: Run Opensearch with A Single Plugin Remote Cluster uses: derek-ho/start-opensearch@graceful-t with: - opensearch-version: ${{ matrix.os_bwc_version }} - plugins: "file:$(pwd)/opensearch-security-${{matrix.os_bwc_version}}.zip" + opensearch-version: ${{ matrix.datasource_version }} + plugins: "file:$(pwd)/opensearch-security-${{matrix.datasource_version}}.zip" security-enabled: true admin-password: ${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} security_config_file: ${{ inputs.security_config_file }} diff --git a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx index a0224be79..20430ba33 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx @@ -37,7 +37,7 @@ import { useToastState } from '../../utils/toast-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; -import { createDataSourceQuery, getClusterInfoIfEnabled } from '../../../../utils/datasource-utils'; +import { createDataSourceQuery, getClusterInfo } from '../../../../utils/datasource-utils'; interface AuditLoggingEditSettingProps extends AppDependencies { setting: 'general' | 'compliance'; @@ -133,12 +133,10 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { }; if (props.setting === 'general') { - addSuccessToast( - `General settings saved ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` - ); + addSuccessToast(`General settings saved ${getClusterInfo(dataSourceEnabled, dataSource)}`); } else { addSuccessToast( - `Compliance settings saved ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` + `Compliance settings saved ${getClusterInfo(dataSourceEnabled, dataSource)}` ); } @@ -149,7 +147,7 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { color: 'danger', iconType: 'alert', title: - `Failed to update audit configuration ${getClusterInfoIfEnabled( + `Failed to update audit configuration ${getClusterInfo( dataSourceEnabled, dataSource )} due to ` + e?.message, diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index dd638f5ce..dffdf1597 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -38,7 +38,7 @@ import { httpDelete } from '../utils/request-utils'; import { createSuccessToast, createUnknownErrorToast, useToastState } from '../utils/toast-utils'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { DataSourceContext } from '../app-router'; -import { getClusterInfoIfEnabled, createDataSourceQuery } from '../../../utils/datasource-utils'; +import { getClusterInfo, createDataSourceQuery } from '../../../utils/datasource-utils'; const addBackendStep = { title: 'Add backends', @@ -256,21 +256,15 @@ export function GetStarted(props: AppDependencies) { addToast( createSuccessToast( 'cache-flush-success', - `Cache purge successful ${getClusterInfoIfEnabled( - dataSourceEnabled, - dataSource - )}`, - `Cache purge successful ${getClusterInfoIfEnabled( - dataSourceEnabled, - dataSource - )}` + `Cache purge successful ${getClusterInfo(dataSourceEnabled, dataSource)}`, + `Cache purge successful ${getClusterInfo(dataSourceEnabled, dataSource)}` ) ); } catch (err) { addToast( createUnknownErrorToast( 'cache-flush-failed', - `purge cache ${getClusterInfoIfEnabled(dataSourceEnabled, dataSource)}` + `purge cache ${getClusterInfo(dataSourceEnabled, dataSource)}` ) ); } diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index 1983fd017..1c554a9a8 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -330,11 +330,7 @@ export function PermissionList(props: AppDependencies) { }; const createActionGroupMenuItems = [ - showEditModal('', Action.create, [])} - > + showEditModal('', Action.create, [])}> Create from blank , { 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'); + expect(getClusterInfo(false, { id: 'blah', label: 'blah' })).toBe(''); + expect(getClusterInfo(true, { id: '', label: '' })).toBe('for Local cluster'); + expect(getClusterInfo(true, { id: 'test', label: 'test' })).toBe('for test'); }); it('Tests the create DataSource query helper function', () => { diff --git a/server/plugin.ts b/server/plugin.ts index e7877ad62..a6d87bea1 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -46,7 +46,6 @@ 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 { @@ -55,7 +54,6 @@ export interface SecurityPluginRequestContext { } export interface SecurityPluginSetupDependencies { - dataSourceManagement: ReturnType; dataSource: DataSourcePluginSetup; } diff --git a/server/routes/index.ts b/server/routes/index.ts index 38d7566bb..6faa939f4 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -945,6 +945,11 @@ export function defineRoutes(router: IRouter, dataSourceEnabled: boolean) { ); } +/** + * A helper function to wrap API calls with the appropriate client. If the multiple datasources feature is disabled, it will use + * the existing client provided by the security plugin. Otherwise, it will use the one provided by the datasource plugin based on the id + * that we extract via the UI. + */ const wrapRouteWithDataSource = async ( dataSourceEnabled: boolean, context: RequestHandlerContext, diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 2f30d5d3c..cdde2df63 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -109,7 +109,7 @@ describe('Multi-datasources enabled', () => { it('Checks Auth Tab', () => { cy.visit(`http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/auth`); - cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(1)'); }); it('Checks Users Tab', () => { From 16b1442d5d570686f2a6b1aa3215e3f4e2b38e67 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 23 Apr 2024 15:32:41 -0400 Subject: [PATCH 14/22] Revert to 2.4 earliest Signed-off-by: Derek Ho --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 78a5b6d50..a4885f02d 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest , windows-latest ] - datasource_version: [1.3.9, 2.4.0, 2.5.0, 2.13.0, 3.0.0] + datasource_version: [2.4.0, 2.13.0, 3.0.0] runs-on: ${{ matrix.os }} steps: From 3d65de48b29d57f3abbd3fc1fb3e494e42688f02 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Tue, 23 Apr 2024 15:52:09 -0400 Subject: [PATCH 15/22] Remove extra in line comments Signed-off-by: Derek Ho --- .../test/internal-user-list-utils.test.tsx | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx index a1a9323c9..74f8e8c64 100644 --- a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx +++ b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx @@ -44,20 +44,14 @@ describe('Internal user list utils', () => { const userType = 'internalaccounts'; const query = { dataSourceId: 'test' }; - // Mock the response data from httpGet const mockRawData = { - data: { - // your mocked data here - }, + data: {}, }; - // Mock the return value of getUserListRaw jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - // Call the function you want to test const test = await getUserList(httpMock, userType, query); - // Assert that httpGet was called with the correct parameters expect(httpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/internalaccounts', @@ -67,22 +61,18 @@ describe('Internal user list utils', () => { }); it('getUserList calls httpGet with the correct parameters for service accounts', async () => { - const httpMock = {}; // Mock HttpStart object + const httpMock = {}; const userType = 'serviceAccounts'; const query = { dataSourceId: 'test' }; - // Mock the response data from httpGet const mockRawData = { data: {}, }; - // Mock the return value of getUserListRaw jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - // Call the function you want to test const test = await getUserList(httpMock, userType, query); - // Assert that httpGet was called with the correct parameters expect(httpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/serviceaccounts', @@ -92,21 +82,17 @@ describe('Internal user list utils', () => { }); it('fetchUserNameList calls httpGet with the correct parameters for service accounts', async () => { - const httpMock = {}; // Mock HttpStart object + const httpMock = {}; const userType = 'serviceAccounts'; - // Mock the response data from httpGet const mockRawData = { data: {}, }; - // Mock the return value of getUserListRaw jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - // Call the function you want to test const test = await fetchUserNameList(httpMock, userType); - // Assert that httpGet was called with the correct parameters expect(httpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/serviceaccounts', @@ -115,21 +101,17 @@ describe('Internal user list utils', () => { }); it('fetchUserNameList calls httpGet with the correct parameters for internal users', async () => { - const httpMock = {}; // Mock HttpStart object + const httpMock = {}; const userType = 'internalaccounts'; - // Mock the response data from httpGet const mockRawData = { data: {}, }; - // Mock the return value of getUserListRaw jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - // Call the function you want to test const test = await fetchUserNameList(httpMock, userType); - // Assert that httpGet was called with the correct parameters expect(httpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/internalaccounts', From b6fd71079d981650e8bb79c111be17ee6dc8347c Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Wed, 24 Apr 2024 10:54:09 -0400 Subject: [PATCH 16/22] Migrate to v4 after new release Signed-off-by: Derek Ho --- .github/workflows/cypress-test-multidatasources-enabled-e2e.yml | 2 +- .github/workflows/integration-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml index 4ae9b05e3..162941cf1 100644 --- a/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml +++ b/.github/workflows/cypress-test-multidatasources-enabled-e2e.yml @@ -67,7 +67,7 @@ jobs: download-location: ${{env.PLUGIN_NAME}} - name: Run Opensearch with A Single Plugin - uses: derek-ho/start-opensearch@graceful-t + uses: derek-ho/start-opensearch@v4 with: opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugins: "file:$(pwd)/opensearch-security.zip" diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index a4885f02d..3324f5376 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -52,7 +52,7 @@ jobs: plugin-version: ${{ env.PLUGIN_VERSION }} - name: Run Opensearch with A Single Plugin Remote Cluster - uses: derek-ho/start-opensearch@graceful-t + uses: derek-ho/start-opensearch@v4 with: opensearch-version: ${{ matrix.datasource_version }} plugins: "file:$(pwd)/opensearch-security-${{matrix.datasource_version}}.zip" From 98addde8155b4ff53c26d824586bd21092a49d30 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 25 Apr 2024 11:01:30 -0400 Subject: [PATCH 17/22] Refactor code to use a request context class Signed-off-by: Derek Ho --- common/index.ts | 2 + cypress.config.js | 4 + public/apps/account/utils.tsx | 22 +++-- .../audit-logging-edit-settings.tsx | 13 +-- .../panels/audit-logging/audit-logging.tsx | 12 +-- .../panels/auth-view/auth-view.tsx | 6 +- .../apps/configuration/panels/get-started.tsx | 7 +- .../internal-user-edit/internal-user-edit.tsx | 10 +-- .../test/internal-user-edit.test.tsx | 4 +- .../permission-list/permission-list.tsx | 14 +-- .../test/permission-list.test.tsx | 9 +- .../panels/role-edit/role-edit.tsx | 14 +-- .../panels/role-edit/test/role-edit.test.tsx | 2 +- .../apps/configuration/panels/role-list.tsx | 17 +--- .../role-mapping/role-edit-mapped-user.tsx | 17 +--- .../test/role-edit-mapped-user.test.tsx | 2 +- .../panels/role-view/role-view.tsx | 29 ++---- .../panels/role-view/tenants-panel.tsx | 3 +- .../__snapshots__/role-view.test.tsx.snap | 3 + .../panels/service-account-list.tsx | 8 +- .../panels/tenant-list/configure_tab1.tsx | 3 +- .../panels/tenant-list/manage_tab.tsx | 4 +- .../panels/test/get-started.test.tsx | 18 ++-- .../apps/configuration/panels/user-list.tsx | 9 +- .../utils/action-groups-utils.tsx | 37 +++----- .../utils/audit-logging-utils.tsx | 19 ++-- .../configuration/utils/auth-view-utils.tsx | 11 ++- .../utils/internal-user-detail-utils.tsx | 14 ++- .../utils/internal-user-list-utils.tsx | 25 +++--- .../apps/configuration/utils/request-utils.ts | 90 +++++++++++-------- .../configuration/utils/role-detail-utils.tsx | 14 ++- .../configuration/utils/role-list-utils.tsx | 28 +++--- .../utils/role-mapping-utils.tsx | 14 ++- .../utils/tenancy-config_util.tsx | 12 +-- .../apps/configuration/utils/tenant-utils.tsx | 44 ++++++--- .../test/internal-user-list-utils.test.tsx | 53 +++-------- .../utils/test/request-utils.test.ts | 45 ++++++++++ public/utils/auth-info-utils.tsx | 9 +- public/utils/dashboards-info-utils.tsx | 11 ++- public/utils/login-utils.tsx | 5 +- public/utils/logout-utils.tsx | 14 ++- .../multi_datasources_enabled.spec.js | 16 ++-- 42 files changed, 357 insertions(+), 336 deletions(-) create mode 100644 public/apps/configuration/utils/test/request-utils.test.ts diff --git a/common/index.ts b/common/index.ts index 1a0eb3ff5..66abaa09f 100644 --- a/common/index.ts +++ b/common/index.ts @@ -56,6 +56,8 @@ export const MAX_INTEGER = 2147483647; export const MAX_LENGTH_OF_COOKIE_BYTES = 4000; export const ESTIMATED_IRON_COOKIE_OVERHEAD = 1.5; +export const LocalClusterId = ''; + export enum AuthType { BASIC = 'basicauth', OPEN_ID = 'openid', diff --git a/cypress.config.js b/cypress.config.js index e5a86728b..6f619e800 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -31,5 +31,9 @@ module.exports = defineConfig({ openSearchUrl: 'https://localhost:9200', adminUserName: 'admin', adminPassword: 'myStrongPassword123!', + externalDataSourceAdminUserName: 'admin', + externalDataSourceAdminPassword: 'myStrongPassword123!', + externalDataSourceLabel: '9202', + externalDataSourceEndpoint: 'https://localhost:9202' }, }); diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index 2f9f25eee..0c82f9e6f 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -14,22 +14,32 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_AUTH_LOGOUT } from '../../../common'; +import { API_AUTH_LOGOUT, LocalClusterId } from '../../../common'; import { setShouldShowTenantPopup } from '../../utils/storage-utils'; -import { httpGet, httpGetWithIgnores, httpPost } from '../configuration/utils/request-utils'; +import { createRequestContextWithDataSourceId } from '../configuration/utils/request-utils'; import { API_ENDPOINT_ACCOUNT_INFO } from './constants'; import { AccountInfo } from './types'; export function fetchAccountInfo(http: HttpStart): Promise { - return httpGet({ http, url: API_ENDPOINT_ACCOUNT_INFO }); + return createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_ACCOUNT_INFO, + }); } export async function fetchAccountInfoSafe(http: HttpStart): Promise { - return httpGetWithIgnores({ http, url: API_ENDPOINT_ACCOUNT_INFO, ignores: [401] }); + return createRequestContextWithDataSourceId(LocalClusterId).httpGetWithIgnores({ + http, + url: API_ENDPOINT_ACCOUNT_INFO, + ignores: [401], + }); } export async function logout(http: HttpStart, logoutUrl?: string): Promise { - await httpPost({ http, url: API_AUTH_LOGOUT }); + await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + http, + url: API_AUTH_LOGOUT, + }); setShouldShowTenantPopup(null); // Clear everything in the sessionStorage since they can contain sensitive information sessionStorage.clear(); @@ -52,7 +62,7 @@ export async function updateNewPassword( newPassword: string, currentPassword: string ): Promise { - await httpPost({ + await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ http, url: API_ENDPOINT_ACCOUNT_INFO, body: { diff --git a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx index 20430ba33..8e89d62a7 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx @@ -37,7 +37,7 @@ import { useToastState } from '../../utils/toast-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; -import { createDataSourceQuery, getClusterInfo } from '../../../../utils/datasource-utils'; +import { getClusterInfo } from '../../../../utils/datasource-utils'; interface AuditLoggingEditSettingProps extends AppDependencies { setting: 'general' | 'compliance'; @@ -68,10 +68,7 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { React.useEffect(() => { const fetchConfig = async () => { try { - const fetchedConfig = await getAuditLogging( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const fetchedConfig = await getAuditLogging(props.coreStart.http, dataSource.id); setEditConfig(fetchedConfig); } catch (e) { console.log(e); @@ -114,11 +111,7 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { const saveConfig = async (configToUpdate: AuditLoggingSettings) => { try { - await updateAuditLogging( - props.coreStart.http, - configToUpdate, - createDataSourceQuery(dataSource.id) - ); + await updateAuditLogging(props.coreStart.http, configToUpdate, dataSource.id); const addSuccessToast = (text: string) => { const successToast: Toast = { diff --git a/public/apps/configuration/panels/audit-logging/audit-logging.tsx b/public/apps/configuration/panels/audit-logging/audit-logging.tsx index 73b5af782..7b3bd5429 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging.tsx @@ -45,7 +45,6 @@ import { ViewSettingGroup } from './view-setting-group'; import { DocLinks } from '../../constants'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface AuditLoggingProps extends AppDependencies { fromType: string; @@ -144,11 +143,7 @@ export function AuditLogging(props: AuditLoggingProps) { const updatedConfiguration = { ...configuration }; updatedConfiguration.enabled = !updatedConfiguration.enabled; - await updateAuditLogging( - props.coreStart.http, - updatedConfiguration, - createDataSourceQuery(dataSource.id) - ); + await updateAuditLogging(props.coreStart.http, updatedConfiguration, dataSource.id); setConfiguration(updatedConfiguration); } catch (e) { @@ -159,10 +154,7 @@ export function AuditLogging(props: AuditLoggingProps) { React.useEffect(() => { const fetchData = async () => { try { - const auditLogging = await getAuditLogging( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const auditLogging = await getAuditLogging(props.coreStart.http, dataSource.id); setConfiguration(auditLogging); } catch (e) { // TODO: switch to better error handling. diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index eec379ead..7a5eb4b9d 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -25,7 +25,6 @@ import { getSecurityConfig } from '../../utils/auth-view-utils'; import { InstructionView } from './instruction-view'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); @@ -37,10 +36,7 @@ export function AuthView(props: AppDependencies) { const fetchData = async () => { try { setLoading(true); - const config = await getSecurityConfig( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const config = await getSecurityConfig(props.coreStart.http, dataSource.id); setAuthentication(config.authc); setAuthorization(config.authz); diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index dffdf1597..4d3b8c461 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -34,11 +34,11 @@ import { Action } from '../types'; import { ResourceType } from '../../../../common'; import { API_ENDPOINT_CACHE, DocLinks } from '../constants'; import { ExternalLink, ExternalLinkButton } from '../utils/display-utils'; -import { httpDelete } from '../utils/request-utils'; +import { createRequestContextWithDataSourceId } from '../utils/request-utils'; import { createSuccessToast, createUnknownErrorToast, useToastState } from '../utils/toast-utils'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { DataSourceContext } from '../app-router'; -import { getClusterInfo, createDataSourceQuery } from '../../../utils/datasource-utils'; +import { getClusterInfo } from '../../../utils/datasource-utils'; const addBackendStep = { title: 'Add backends', @@ -248,10 +248,9 @@ export function GetStarted(props: AppDependencies) { data-test-subj="purge-cache" onClick={async () => { try { - await httpDelete({ + await createRequestContextWithDataSourceId(dataSource.id).httpDelete({ http: props.coreStart.http, url: API_ENDPOINT_CACHE, - query: createDataSourceQuery(dataSource.id), }); addToast( createSuccessToast( diff --git a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx index 70a07452a..70ba39be8 100644 --- a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx +++ b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx @@ -49,7 +49,6 @@ import { constructErrorMessageAndLog } from '../../../error-utils'; import { BackendRolePanel } from './backend-role-panel'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface InternalUserEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -86,7 +85,7 @@ export function InternalUserEdit(props: InternalUserEditDeps) { const user = await getUserDetail( props.coreStart.http, props.sourceUserName, - createDataSourceQuery(dataSource.id) + dataSource.id ); setAttributes(buildAttributeState(user.attributes)); setBackendRoles(user.backend_roles); @@ -126,12 +125,7 @@ export function InternalUserEdit(props: InternalUserEditDeps) { updateObject.password = password; } - await updateUser( - props.coreStart.http, - userName, - updateObject, - createDataSourceQuery(dataSource.id) - ); + await updateUser(props.coreStart.http, userName, updateObject, dataSource.id); setCrossPageToast(buildUrl(ResourceType.users), { id: 'updateUserSucceeded', diff --git a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx index c77b294bf..241f4e14f 100644 --- a/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx +++ b/public/apps/configuration/panels/internal-user-edit/test/internal-user-edit.test.tsx @@ -83,9 +83,7 @@ describe('Internal user edit', () => { /> ); - expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername, { - dataSourceId: 'test', - }); + expect(getUserDetail).toBeCalledWith(mockCoreStart.http, sampleUsername, 'test'); }); it('should not submit if password is empty on creation', () => { diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index 1c554a9a8..e89bb2d03 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -63,7 +63,6 @@ import { generateResourceName } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; export function renderBooleanToCheckMark(value: boolean): React.ReactNode { return value ? : ''; @@ -207,10 +206,7 @@ export function PermissionList(props: AppDependencies) { const fetchData = useCallback(async () => { try { setLoading(true); - const actionGroups = await fetchActionGroups( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const actionGroups = await fetchActionGroups(props.coreStart.http, dataSource.id); setActionGroupDict(actionGroups); setPermissionList(await mergeAllPermissions(actionGroups)); } catch (e) { @@ -228,11 +224,7 @@ export function PermissionList(props: AppDependencies) { const handleDelete = async () => { const groupsToDelete: string[] = selection.map((r) => r.name); try { - await requestDeleteActionGroups( - props.coreStart.http, - groupsToDelete, - createDataSourceQuery(dataSource.id) - ); + await requestDeleteActionGroups(props.coreStart.http, groupsToDelete, dataSource.id); setPermissionList(difference(permissionList, selection)); setSelection([]); } catch (e) { @@ -300,7 +292,7 @@ export function PermissionList(props: AppDependencies) { props.coreStart.http, groupName, { allowed_actions: allowedAction }, - createDataSourceQuery(dataSource.id) + dataSource.id ); setEditModal(null); fetchData(); diff --git a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx index a15d2eace..0d72b6ea4 100644 --- a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx +++ b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx @@ -102,9 +102,6 @@ describe('Permission list page ', () => { const mockCoreStart = { http: 1, }; - const dataSourceQuery = { - dataSourceId: 'test', - }; it('render empty', () => { const component = shallow( { /> ); - expect(fetchActionGroups).toBeCalledWith(mockCoreStart.http, dataSourceQuery); + expect(fetchActionGroups).toBeCalledWith(mockCoreStart.http, 'test'); }); it('fetch data error', () => { @@ -172,7 +169,7 @@ describe('Permission list page ', () => { mockCoreStart.http, 'group1', { allowed_actions: [] }, - dataSourceQuery + 'test' ); }); @@ -213,7 +210,7 @@ describe('Permission list page ', () => { deleteFunc(); process.nextTick(() => { - expect(requestDeleteActionGroups).toBeCalledWith(mockCoreStart.http, [], dataSourceQuery); + expect(requestDeleteActionGroups).toBeCalledWith(mockCoreStart.http, [], 'test'); done(); }); }); diff --git a/public/apps/configuration/panels/role-edit/role-edit.tsx b/public/apps/configuration/panels/role-edit/role-edit.tsx index 6fdc3b281..42fb8c2a9 100644 --- a/public/apps/configuration/panels/role-edit/role-edit.tsx +++ b/public/apps/configuration/panels/role-edit/role-edit.tsx @@ -59,7 +59,6 @@ import { generateResourceName } from '../../utils/resource-utils'; import { NameRow } from '../../utils/name-row'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery } from '../../../../utils/datasource-utils'; interface RoleEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -100,7 +99,7 @@ export function RoleEdit(props: RoleEditDeps) { const roleData = await getRoleDetail( props.coreStart.http, props.sourceRoleName, - createDataSourceQuery(dataSource.id) + dataSource.id ); setRoleClusterPermission(roleData.cluster_permissions.map(stringToComboBoxOption)); setRoleIndexPermission(buildIndexPermissionState(roleData.index_permissions)); @@ -121,10 +120,7 @@ export function RoleEdit(props: RoleEditDeps) { React.useEffect(() => { const fetchActionGroupNames = async () => { try { - const actionGroupsObject = await fetchActionGroups( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const actionGroupsObject = await fetchActionGroups(props.coreStart.http, dataSource.id); setActionGroups(Object.entries(actionGroupsObject)); } catch (e) { addToast(createUnknownErrorToast('actionGroup', 'load data')); @@ -139,9 +135,7 @@ export function RoleEdit(props: RoleEditDeps) { React.useEffect(() => { const fetchTenantNames = async () => { try { - setTenantNames( - await fetchTenantNameList(props.coreStart.http, createDataSourceQuery(dataSource.id)) - ); + setTenantNames(await fetchTenantNameList(props.coreStart.http, dataSource.id)); } catch (e) { addToast(createUnknownErrorToast('tenant', 'load data')); console.error(e); @@ -169,7 +163,7 @@ export function RoleEdit(props: RoleEditDeps) { index_permissions: unbuildIndexPermissionState(validIndexPermission), tenant_permissions: unbuildTenantPermissionState(validTenantPermission), }, - createDataSourceQuery(dataSource.id) + dataSource.id ); setCrossPageToast(buildUrl(ResourceType.roles, Action.view, roleName), { diff --git a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx index 4bc01ebe2..6e0b61237 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx @@ -128,7 +128,7 @@ describe('Role edit', () => { index_permissions: [], tenant_permissions: [], }, - { dataSourceId: 'test' } + 'test' ); process.nextTick(() => { diff --git a/public/apps/configuration/panels/role-list.tsx b/public/apps/configuration/panels/role-list.tsx index 0603c715d..7bdf66593 100644 --- a/public/apps/configuration/panels/role-list.tsx +++ b/public/apps/configuration/panels/role-list.tsx @@ -56,7 +56,6 @@ import { useContextMenuState } from '../utils/context-menu'; import { DocLinks } from '../constants'; import { DataSourceContext } from '../app-router'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; -import { createDataSourceQuery } from '../../../utils/datasource-utils'; const columns: Array> = [ { @@ -114,14 +113,8 @@ export function RoleList(props: AppDependencies) { const fetchData = async () => { try { setLoading(true); - const rawRoleData = await fetchRole( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); - const rawRoleMappingData = await fetchRoleMapping( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const rawRoleData = await fetchRole(props.coreStart.http, dataSource.id); + const rawRoleMappingData = await fetchRoleMapping(props.coreStart.http, dataSource.id); const processedData = transformRoleData(rawRoleData, rawRoleMappingData); setRoleData(processedData); } catch (e) { @@ -138,11 +131,7 @@ export function RoleList(props: AppDependencies) { const handleDelete = async () => { const rolesToDelete: string[] = selection.map((r) => r.roleName); try { - await requestDeleteRoles( - props.coreStart.http, - rolesToDelete, - createDataSourceQuery(dataSource.id) - ); + await requestDeleteRoles(props.coreStart.http, rolesToDelete, dataSource.id); // Refresh from server (calling fetchData) does not work here, the server still return the roles // that had been just deleted, probably because ES takes some time to sync to all nodes. // So here remove the selected roles from local memory directly. diff --git a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx index d3f774ac8..8bfc51b01 100644 --- a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx +++ b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx @@ -45,7 +45,7 @@ import { setCrossPageToast } from '../../utils/storage-utils'; import { ExternalLink } from '../../utils/display-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; -import { createDataSourceQuery, getClusterInfo } from '../../../../utils/datasource-utils'; +import { getClusterInfo } from '../../../../utils/datasource-utils'; interface RoleEditMappedUserProps extends BreadcrumbsPageDependencies { roleName: string; @@ -72,7 +72,7 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { const originalRoleMapData: RoleMappingDetail | undefined = await getRoleMappingData( props.coreStart.http, props.roleName, - createDataSourceQuery(dataSource.id) + dataSource.id ); if (originalRoleMapData) { setInternalUsers(originalRoleMapData.users.map(stringToComboBoxOption)); @@ -92,11 +92,7 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { const fetchInternalUserNames = async () => { try { setUserNames( - await fetchUserNameList( - props.coreStart.http, - ResourceType.users, - createDataSourceQuery(dataSource.id) - ) + await fetchUserNameList(props.coreStart.http, ResourceType.users, dataSource.id) ); } catch (e) { addToast(createUnknownErrorToast('fetchInternalUserNames', 'load data')); @@ -120,12 +116,7 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { hosts, }; - await updateRoleMapping( - props.coreStart.http, - props.roleName, - updateObject, - createDataSourceQuery(dataSource.id) - ); + await updateRoleMapping(props.coreStart.http, props.roleName, updateObject, dataSource.id); setCrossPageToast( buildUrl(ResourceType.roles, Action.view, props.roleName, SubAction.mapuser), { diff --git a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx index e78435817..44d4d0a2f 100644 --- a/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx +++ b/public/apps/configuration/panels/role-mapping/test/role-edit-mapped-user.test.tsx @@ -118,7 +118,7 @@ describe('Role mapping edit', () => { backend_roles: [], hosts: [], }, - { dataSourceId: 'test' } + 'test' ); }); diff --git a/public/apps/configuration/panels/role-view/role-view.tsx b/public/apps/configuration/panels/role-view/role-view.tsx index 50adcaf00..caac1bfd7 100644 --- a/public/apps/configuration/panels/role-view/role-view.tsx +++ b/public/apps/configuration/panels/role-view/role-view.tsx @@ -71,7 +71,7 @@ import { requestDeleteRoles } from '../../utils/role-list-utils'; import { setCrossPageToast } from '../../utils/storage-utils'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; -import { createDataSourceQuery, getClusterInfo } from '../../../../utils/datasource-utils'; +import { getClusterInfo } from '../../../../utils/datasource-utils'; interface RoleViewProps extends BreadcrumbsPageDependencies { roleName: string; @@ -123,23 +123,16 @@ export function RoleView(props: RoleViewProps) { const originalRoleMapData = await getRoleMappingData( props.coreStart.http, props.roleName, - createDataSourceQuery(dataSource.id) + dataSource.id ); if (originalRoleMapData) { setMappedUsers(transformRoleMappingData(originalRoleMapData)); setHosts(originalRoleMapData.hosts); } - const actionGroups = await fetchActionGroups( - props.coreStart.http, - createDataSourceQuery(dataSource.id) - ); + const actionGroups = await fetchActionGroups(props.coreStart.http, dataSource.id); setActionGroupDict(actionGroups); - const roleData = await getRoleDetail( - props.coreStart.http, - props.roleName, - createDataSourceQuery(dataSource.id) - ); + const roleData = await getRoleDetail(props.coreStart.http, props.roleName, dataSource.id); setIsReserved(roleData.reserved); setRoleClusterPermission(roleData.cluster_permissions); setRoleIndexPermission(transformRoleIndexPermissions(roleData.index_permissions)); @@ -170,12 +163,7 @@ export function RoleView(props: RoleViewProps) { backend_roles: difference(externalIdentities, usersToDelete), hosts, }; - await updateRoleMapping( - props.coreStart.http, - props.roleName, - updateObject, - createDataSourceQuery(dataSource.id) - ); + await updateRoleMapping(props.coreStart.http, props.roleName, updateObject, dataSource.id); setMappedUsers(difference(mappedUsers, selection)); setSelection([]); @@ -283,6 +271,7 @@ export function RoleView(props: RoleViewProps) { coreStart={props.coreStart} loading={loading} isReserved={isReserved} + dataSourceId={dataSource.id} /> ), @@ -370,11 +359,7 @@ export function RoleView(props: RoleViewProps) { color="danger" onClick={async () => { try { - await requestDeleteRoles( - props.coreStart.http, - [props.roleName], - createDataSourceQuery(dataSource.id) - ); + await requestDeleteRoles(props.coreStart.http, [props.roleName], dataSource.id); setCrossPageToast(buildUrl(ResourceType.roles), { id: 'deleteRole', color: 'success', diff --git a/public/apps/configuration/panels/role-view/tenants-panel.tsx b/public/apps/configuration/panels/role-view/tenants-panel.tsx index d0cde8cd9..2e5094028 100644 --- a/public/apps/configuration/panels/role-view/tenants-panel.tsx +++ b/public/apps/configuration/panels/role-view/tenants-panel.tsx @@ -49,6 +49,7 @@ interface RoleViewTenantsPanelProps { coreStart: CoreStart; loading: boolean; isReserved: boolean; + dataSourceId: string; } export function TenantsPanel(props: RoleViewTenantsPanelProps) { @@ -62,7 +63,7 @@ export function TenantsPanel(props: RoleViewTenantsPanelProps) { React.useEffect(() => { const fetchData = async () => { try { - const rawTenantData = await fetchTenants(props.coreStart.http); + const rawTenantData = await fetchTenants(props.coreStart.http, props.dataSourceId); const processedTenantData = transformTenantData(rawTenantData); setTenantPermissionDetail( transformRoleTenantPermissionData(props.tenantPermissions, processedTenantData) diff --git a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap index c5095452f..833c64f6f 100644 --- a/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap +++ b/public/apps/configuration/panels/role-view/test/__snapshots__/role-view.test.tsx.snap @@ -114,6 +114,7 @@ exports[`Role view basic rendering when permission tab is selected 1`] = ` "http": 1, } } + dataSourceId="test" errorFlag={false} isReserved={false} loading={false} @@ -163,6 +164,7 @@ exports[`Role view basic rendering when permission tab is selected 1`] = ` "http": 1, } } + dataSourceId="test" errorFlag={false} isReserved={false} loading={false} @@ -580,6 +582,7 @@ exports[`Role view renders when mapped user tab is selected 1`] = ` "http": 1, } } + dataSourceId="test" errorFlag={false} isReserved={false} loading={false} diff --git a/public/apps/configuration/panels/service-account-list.tsx b/public/apps/configuration/panels/service-account-list.tsx index 2ebf47662..6ce2e74f3 100644 --- a/public/apps/configuration/panels/service-account-list.tsx +++ b/public/apps/configuration/panels/service-account-list.tsx @@ -35,7 +35,7 @@ import { getAuthInfo } from '../../../utils/auth-info-utils'; import { AppDependencies } from '../../types'; import { API_ENDPOINT_SERVICEACCOUNTS, DocLinks } from '../constants'; import { Action } from '../types'; -import { ResourceType } from '../../../../common'; +import { LocalClusterId, ResourceType } from '../../../../common'; import { EMPTY_FIELD_VALUE } from '../ui-constants'; import { useContextMenuState } from '../utils/context-menu'; import { ExternalLink, tableItemsUIProps, truncatedListView } from '../utils/display-utils'; @@ -104,7 +104,11 @@ export function ServiceAccountList(props: AppDependencies) { const fetchData = async () => { try { setLoading(true); - const userDataPromise = getUserList(props.coreStart.http, ResourceType.serviceAccounts); + const userDataPromise = getUserList( + props.coreStart.http, + ResourceType.serviceAccounts, + LocalClusterId + ); setCurrentUsername((await getAuthInfo(props.coreStart.http)).user_name); setUserData(await userDataPromise); } catch (e) { diff --git a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx index dafd153d6..2afa65580 100644 --- a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx +++ b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx @@ -49,6 +49,7 @@ import { useToastState, } from '../../utils/toast-utils'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; +import { LocalClusterId } from '../../../../../common'; export function ConfigureTab1(props: AppDependencies) { const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(false); @@ -191,7 +192,7 @@ export function ConfigureTab1(props: AppDependencies) { default_tenant: (await getDashboardsInfo(props.coreStart.http)).default_tenant, }); - const rawTenantData = await fetchTenants(props.coreStart.http); + const rawTenantData = await fetchTenants(props.coreStart.http, LocalClusterId); const processedTenantData = transformTenantData(rawTenantData); setTenantData(processedTenantData); } catch (e) { diff --git a/public/apps/configuration/panels/tenant-list/manage_tab.tsx b/public/apps/configuration/panels/tenant-list/manage_tab.tsx index f6689ac95..8031b3ce4 100644 --- a/public/apps/configuration/panels/tenant-list/manage_tab.tsx +++ b/public/apps/configuration/panels/tenant-list/manage_tab.tsx @@ -42,7 +42,7 @@ import { flow } from 'lodash'; import { getCurrentUser } from '../../../../utils/auth-info-utils'; import { AppDependencies } from '../../../types'; import { Action, Tenant } from '../../types'; -import { ResourceType } from '../../../../../common'; +import { LocalClusterId, ResourceType } from '../../../../../common'; import { ExternalLink, renderCustomization, tableItemsUIProps } from '../../utils/display-utils'; import { fetchTenants, @@ -97,7 +97,7 @@ export function ManageTab(props: AppDependencies) { const fetchData = useCallback(async () => { try { setLoading(true); - const rawTenantData = await fetchTenants(http); + const rawTenantData = await fetchTenants(http, LocalClusterId); const processedTenantData = transformTenantData(rawTenantData); const activeTenant = await fetchCurrentTenant(http); const currentUser = await getCurrentUser(http); diff --git a/public/apps/configuration/panels/test/get-started.test.tsx b/public/apps/configuration/panels/test/get-started.test.tsx index 00e73a542..0823cdbb9 100644 --- a/public/apps/configuration/panels/test/get-started.test.tsx +++ b/public/apps/configuration/panels/test/get-started.test.tsx @@ -22,6 +22,7 @@ import { buildHashUrl } from '../../utils/url-builder'; import { GetStarted } from '../get-started'; import * as ToastUtils from '../../utils/toast-utils'; // Import all functions from toast-utils import * as RequestUtils from '../../utils/request-utils'; // Import all functions from request-utils +import { RequestContext } from '../../utils/request-utils'; jest.mock('../../utils/toast-utils', () => ({ createSuccessToast: jest.fn(), @@ -29,10 +30,6 @@ jest.mock('../../utils/toast-utils', () => ({ useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]), })); -jest.mock('../../utils/request-utils', () => ({ - httpDelete: jest.fn(), -})); - jest.mock('react', () => ({ ...jest.requireActual('react'), useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function @@ -158,8 +155,10 @@ describe('Get started (landing page)', () => { expect(button).toHaveLength(1); // Failure case: Mock httpDelete to reject + // Success case: Mock httpDelete to resolve + const mockRequestContext = new RequestContext('dummyDataSourceId'); jest - .spyOn(RequestUtils, 'httpDelete') + .spyOn(mockRequestContext, 'httpDelete') .mockRejectedValueOnce(new Error('Failed to purge cache')); await button.props().onClick(); // Simulate button click @@ -171,7 +170,14 @@ describe('Get started (landing page)', () => { expect(button).toHaveLength(1); // Success case: Mock httpDelete to resolve - jest.spyOn(RequestUtils, 'httpDelete').mockResolvedValueOnce('nice'); + const mockRequestContext = new RequestContext('dummyDataSourceId'); + jest.spyOn(mockRequestContext, 'httpDelete').mockResolvedValueOnce('nice'); + + // Mock the createRequestContextWithDataSourceId function to return the mock instance + jest + .spyOn(RequestUtils, 'createRequestContextWithDataSourceId') + .mockReturnValue(mockRequestContext); + await button.props().onClick(); // Simulate button click expect(ToastUtils.createSuccessToast).toHaveBeenCalledTimes(1); }); diff --git a/public/apps/configuration/panels/user-list.tsx b/public/apps/configuration/panels/user-list.tsx index eef98c64b..0d8c20015 100644 --- a/public/apps/configuration/panels/user-list.tsx +++ b/public/apps/configuration/panels/user-list.tsx @@ -50,7 +50,6 @@ import { showTableStatusMessage } from '../utils/loading-spinner-utils'; import { buildHashUrl } from '../utils/url-builder'; import { DataSourceContext } from '../app-router'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; -import { createDataSourceQuery } from '../../../utils/datasource-utils'; export function dictView(items: Dictionary) { if (isEmpty(items)) { @@ -115,7 +114,7 @@ export function UserList(props: AppDependencies) { const userDataPromise = getUserList( props.coreStart.http, ResourceType.users, - createDataSourceQuery(dataSource.id) + dataSource.id ); setCurrentUsername((await getAuthInfo(props.coreStart.http)).user_name); setUserData(await userDataPromise); @@ -133,11 +132,7 @@ export function UserList(props: AppDependencies) { const handleDelete = async () => { const usersToDelete: string[] = selection.map((r) => r.username); try { - await requestDeleteUsers( - props.coreStart.http, - usersToDelete, - createDataSourceQuery(dataSource.id) - ); + await requestDeleteUsers(props.coreStart.http, usersToDelete, dataSource.id); // Refresh from server (calling fetchData) does not work here, the server still return the users // that had been just deleted, probably because ES takes some time to sync to all nodes. // So here remove the selected users from local memory directly. diff --git a/public/apps/configuration/utils/action-groups-utils.tsx b/public/apps/configuration/utils/action-groups-utils.tsx index 7852f86ba..780bee9ec 100644 --- a/public/apps/configuration/utils/action-groups-utils.tsx +++ b/public/apps/configuration/utils/action-groups-utils.tsx @@ -13,11 +13,11 @@ * permissions and limitations under the License. */ -import { HttpStart, HttpFetchQuery } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { map } from 'lodash'; import { API_ENDPOINT_ACTIONGROUPS, CLUSTER_PERMISSIONS, INDEX_PERMISSIONS } from '../constants'; import { DataObject, ActionGroupItem, ActionGroupUpdate, ObjectsMessage } from '../types'; -import { httpDelete, httpGet, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export interface PermissionListingItem { @@ -31,12 +31,13 @@ export interface PermissionListingItem { export async function fetchActionGroups( http: HttpStart, - query: HttpFetchQuery + dataSourceId: string ): Promise> { - const actiongroups = await httpGet>({ + const actiongroups = await createRequestContextWithDataSourceId(dataSourceId).httpGet< + ObjectsMessage + >({ http, url: API_ENDPOINT_ACTIONGROUPS, - query, }); return actiongroups.data; } @@ -54,13 +55,6 @@ export function transformActionGroupsToListingFormat( })); } -export async function fetchActionGroupListing( - http: HttpStart, - query: HttpFetchQuery -): Promise { - return transformActionGroupsToListingFormat(await fetchActionGroups(http, query)); -} - function getClusterSinglePermissions(): PermissionListingItem[] { return CLUSTER_PERMISSIONS.map((permission) => ({ name: permission, @@ -83,13 +77,6 @@ function getIndexSinglePermissions(): PermissionListingItem[] { })); } -export async function getAllPermissionsListing( - http: HttpStart, - query: HttpFetchQuery -): Promise { - return mergeAllPermissions(await fetchActionGroups(http, query)); -} - export async function mergeAllPermissions( actionGroups: DataObject ): Promise { @@ -102,22 +89,24 @@ export async function updateActionGroup( http: HttpStart, groupName: string, updateObject: ActionGroupUpdate, - query: HttpFetchQuery + dataSourceId: string ): Promise { - return await httpPost({ + return await createRequestContextWithDataSourceId(dataSourceId).httpPost({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), body: updateObject, - query, }); } export async function requestDeleteActionGroups( http: HttpStart, groups: string[], - query: HttpFetchQuery + dataSourceId: string ) { for (const group of groups) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group), query }); + await createRequestContextWithDataSourceId(dataSourceId).httpDelete({ + http, + url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group), + }); } } diff --git a/public/apps/configuration/utils/audit-logging-utils.tsx b/public/apps/configuration/utils/audit-logging-utils.tsx index 517babf95..9dda1d35a 100644 --- a/public/apps/configuration/utils/audit-logging-utils.tsx +++ b/public/apps/configuration/utils/audit-logging-utils.tsx @@ -13,23 +13,30 @@ * permissions and limitations under the License. */ -import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { AuditLoggingSettings } from '../panels/audit-logging/types'; import { API_ENDPOINT_AUDITLOGGING, API_ENDPOINT_AUDITLOGGING_UPDATE } from '../constants'; -import { httpGet, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; export async function updateAuditLogging( http: HttpStart, updateObject: AuditLoggingSettings, - query: HttpFetchQuery + dataSourceId: string ) { - return await httpPost({ http, url: API_ENDPOINT_AUDITLOGGING_UPDATE, body: updateObject, query }); + return await createRequestContextWithDataSourceId(dataSourceId).httpPost({ + http, + url: API_ENDPOINT_AUDITLOGGING_UPDATE, + body: updateObject, + }); } export async function getAuditLogging( http: HttpStart, - query: HttpFetchQuery + dataSourceId: string ): Promise { - const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_AUDITLOGGING, query }); + const rawConfiguration = await createRequestContextWithDataSourceId(dataSourceId).httpGet({ + http, + url: API_ENDPOINT_AUDITLOGGING, + }); return rawConfiguration?.config; } diff --git a/public/apps/configuration/utils/auth-view-utils.tsx b/public/apps/configuration/utils/auth-view-utils.tsx index 4a4d168e8..a14165fb2 100644 --- a/public/apps/configuration/utils/auth-view-utils.tsx +++ b/public/apps/configuration/utils/auth-view-utils.tsx @@ -13,11 +13,14 @@ * permissions and limitations under the License. */ -import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_SECURITYCONFIG } from '../constants'; -import { httpGet } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; -export async function getSecurityConfig(http: HttpStart, query?: HttpFetchQuery) { - const rawSecurityConfig = await httpGet({ http, url: API_ENDPOINT_SECURITYCONFIG, query }); +export async function getSecurityConfig(http: HttpStart, dataSourceId: string) { + const rawSecurityConfig = await createRequestContextWithDataSourceId(dataSourceId).httpGet({ + http, + url: API_ENDPOINT_SECURITYCONFIG, + }); return rawSecurityConfig.data.config.dynamic; } diff --git a/public/apps/configuration/utils/internal-user-detail-utils.tsx b/public/apps/configuration/utils/internal-user-detail-utils.tsx index d51ad526a..802e5a1f7 100644 --- a/public/apps/configuration/utils/internal-user-detail-utils.tsx +++ b/public/apps/configuration/utils/internal-user-detail-utils.tsx @@ -13,21 +13,20 @@ * permissions and limitations under the License. */ -import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_INTERNALUSERS } from '../constants'; import { InternalUser, InternalUserUpdate } from '../types'; -import { httpGet, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export async function getUserDetail( http: HttpStart, username: string, - query: HttpFetchQuery + dataSourceId: string ): Promise { - return await httpGet({ + return await createRequestContextWithDataSourceId(dataSourceId).httpGet({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), - query, }); } @@ -35,12 +34,11 @@ export async function updateUser( http: HttpStart, username: string, updateObject: InternalUserUpdate, - query: HttpFetchQuery + dataSourceId: string ): Promise { - return await httpPost({ + return await createRequestContextWithDataSourceId(dataSourceId).httpPost({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, username), body: updateObject, - query, }); } diff --git a/public/apps/configuration/utils/internal-user-list-utils.tsx b/public/apps/configuration/utils/internal-user-list-utils.tsx index 3f3e73e1c..feabd585a 100644 --- a/public/apps/configuration/utils/internal-user-list-utils.tsx +++ b/public/apps/configuration/utils/internal-user-list-utils.tsx @@ -14,7 +14,7 @@ */ import { map } from 'lodash'; -import { HttpFetchQuery, HttpStart } from '../../../../../../src/core/public'; +import { HttpStart } from '../../../../../../src/core/public'; import { API_ENDPOINT_INTERNALACCOUNTS, API_ENDPOINT_INTERNALUSERS, @@ -22,7 +22,7 @@ import { } from '../constants'; import { DataObject, InternalUser, ObjectsMessage } from '../types'; import { ResourceType } from '../../../../common'; -import { httpDelete, httpGet } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export interface InternalUsersListing extends InternalUser { @@ -37,38 +37,43 @@ export function transformUserData(rawData: DataObject): InternalUs })); } -export async function requestDeleteUsers(http: HttpStart, users: string[], query: HttpFetchQuery) { +export async function requestDeleteUsers(http: HttpStart, users: string[], dataSourceId: string) { for (const user of users) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user), query }); + await createRequestContextWithDataSourceId(dataSourceId).httpDelete({ + http, + url: getResourceUrl(API_ENDPOINT_INTERNALUSERS, user), + }); } } export async function getUserListRaw( http: HttpStart, userType: string, - query?: HttpFetchQuery + dataSourceId: string ): Promise> { let ENDPOINT = API_ENDPOINT_INTERNALACCOUNTS; if (userType === ResourceType.serviceAccounts) { ENDPOINT = API_ENDPOINT_SERVICEACCOUNTS; } - return await httpGet>({ http, url: ENDPOINT, query }); + return await createRequestContextWithDataSourceId(dataSourceId).httpGet< + ObjectsMessage + >({ http, url: ENDPOINT }); } export async function getUserList( http: HttpStart, userType: string, - query?: HttpFetchQuery + dataSourceId: string ): Promise { - const rawData = await getUserListRaw(http, userType, query); + const rawData = await getUserListRaw(http, userType, dataSourceId); return transformUserData(rawData.data); } export async function fetchUserNameList( http: HttpStart, userType: string, - query?: HttpFetchQuery + dataSourceId: string ): Promise { - return Object.keys((await getUserListRaw(http, userType, query)).data); + return Object.keys((await getUserListRaw(http, userType, dataSourceId)).data); } diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index 676c1e0f9..419e4f4d6 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -19,41 +19,72 @@ interface RequestType { http: HttpStart; url: string; body?: object; - query?: HttpFetchQuery; } interface RequestParams { requestFunc: HttpHandler; url: string; body?: object; - query?: HttpFetchQuery; + query: HttpFetchQuery; } -export async function request(params: RequestParams): Promise { - const { requestFunc, url, body, query } = params; - if (body) { - return (await requestFunc(url, { body: JSON.stringify(body), query })) as T; +export function createRequestContextWithDataSourceId(dataSourceId: string) { + if (dataSourceId === undefined) { + throw new Error('dataSourceId is not present'); } - return (await requestFunc(url, { query })) as T; + return new RequestContext(dataSourceId); } -export async function httpGet(params: RequestType): Promise { - const { http, url, body, query } = params; - return await request({ requestFunc: http.get, url, body, query }); -} +export class RequestContext { + query: HttpFetchQuery; + constructor(private readonly dataSourceId: string) { + this.query = { + dataSourceId: this.dataSourceId, + }; + } -export async function httpPost(params: RequestType): Promise { - const { http, url, body, query } = params; - return await request({ requestFunc: http.post, url, body, query }); -} + public async httpGet(params: RequestType): Promise { + const { http, url, body } = params; + return await request({ requestFunc: http.get, url, body, query: this.query }); + } + + public async httpPost(params: RequestType): Promise { + const { http, url, body } = params; + return await request({ requestFunc: http.post, url, body, query: this.query }); + } + + public async httpPut(http: HttpStart, url: string, body?: object): Promise { + return await request({ requestFunc: http.put, url, body, query: this.query }); + } + + public async httpDelete(params: RequestType): Promise { + const { http, url, body } = params; + return await request({ requestFunc: http.delete, url, body, query: this.query }); + } + + public async httpDeleteWithIgnores(params: RequestTypeWithIgnore): Promise { + const { http, url, ignores } = params; + return await requestWithIgnores({ + requestFunc: http.delete, + url, + ignores, + query: this.query, + }); + } -export async function httpPut(http: HttpStart, url: string, body?: object): Promise { - return await request({ requestFunc: http.put, url, body }); + public async httpGetWithIgnores(params: RequestTypeWithIgnore): Promise { + const { http, url, ignores } = params; + return await requestWithIgnores({ requestFunc: http.get, url, ignores, query: this.query }); + } } -export async function httpDelete(params: RequestType): Promise { - const { http, url, body, query } = params; - return await request({ requestFunc: http.delete, url, body, query }); +export async function request(params: RequestParams): Promise { + console.log(params); + const { requestFunc, url, body, query } = params; + if (body) { + return (await requestFunc(url, { body: JSON.stringify(body), query })) as T; + } + return (await requestFunc(url, { query })) as T; } interface RequestTypeWithIgnore extends RequestType { @@ -81,22 +112,3 @@ export async function requestWithIgnores( } } } - -export async function httpGetWithIgnores(params: RequestTypeWithIgnore): Promise { - const { http, url, ignores, query } = params; - return await requestWithIgnores({ requestFunc: http.get, url, ignores, query }); -} - -export async function httpPostWithIgnores( - params: RequestTypeWithIgnore -): Promise { - const { http, url, ignores, query } = params; - return await requestWithIgnores({ requestFunc: http.post, url, ignores, query }); -} - -export async function httpDeleteWithIgnores( - params: RequestTypeWithIgnore -): Promise { - const { http, url, ignores, query } = params; - return await requestWithIgnores({ requestFunc: http.delete, url, ignores, query }); -} diff --git a/public/apps/configuration/utils/role-detail-utils.tsx b/public/apps/configuration/utils/role-detail-utils.tsx index 6bd011742..2b4415f40 100644 --- a/public/apps/configuration/utils/role-detail-utils.tsx +++ b/public/apps/configuration/utils/role-detail-utils.tsx @@ -13,21 +13,20 @@ * permissions and limitations under the License. */ -import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_ROLES } from '../constants'; import { RoleDetail, RoleUpdate } from '../types'; -import { httpGet, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export async function getRoleDetail( http: HttpStart, roleName: string, - query: HttpFetchQuery + dataSourceId: string ): Promise { - return await httpGet({ + return await createRequestContextWithDataSourceId(dataSourceId).httpGet({ http, url: getResourceUrl(API_ENDPOINT_ROLES, roleName), - query, }); } @@ -35,12 +34,11 @@ export async function updateRole( http: HttpStart, roleName: string, updateObject: RoleUpdate, - query: HttpFetchQuery + dataSourceId: string ) { - return await httpPost({ + return await createRequestContextWithDataSourceId(dataSourceId).httpPost({ http, url: getResourceUrl(API_ENDPOINT_ROLES, roleName), body: updateObject, - query, }); } diff --git a/public/apps/configuration/utils/role-list-utils.tsx b/public/apps/configuration/utils/role-list-utils.tsx index 5092085dc..4137b0179 100644 --- a/public/apps/configuration/utils/role-list-utils.tsx +++ b/public/apps/configuration/utils/role-list-utils.tsx @@ -14,9 +14,9 @@ */ import { map, chain } from 'lodash'; -import { HttpStart, HttpFetchQuery } from '../../../../../../src/core/public'; +import { HttpStart } from '../../../../../../src/core/public'; import { API_ENDPOINT_ROLES, API_ENDPOINT_ROLESMAPPING } from '../constants'; -import { httpDelete, httpDeleteWithIgnores, httpGet } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export interface RoleListing { @@ -92,24 +92,32 @@ export function buildSearchFilterOptions(roleList: any[], attrName: string): Arr } // Submit request to delete given roles. No error handling in this function. -export async function requestDeleteRoles(http: HttpStart, roles: string[], query: HttpFetchQuery) { +export async function requestDeleteRoles(http: HttpStart, roles: string[], dataSourceId: string) { for (const role of roles) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ROLES, role), query }); - await httpDeleteWithIgnores({ + await createRequestContextWithDataSourceId(dataSourceId).httpDelete({ + http, + url: getResourceUrl(API_ENDPOINT_ROLES, role), + }); + await createRequestContextWithDataSourceId(dataSourceId).httpDeleteWithIgnores({ http, url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, role), ignores: [404], - query, }); } } // TODO: have a type definition for it -export function fetchRole(http: HttpStart, query: HttpFetchQuery): Promise { - return httpGet({ http, url: API_ENDPOINT_ROLES, query }); +export function fetchRole(http: HttpStart, dataSourceId: string): Promise { + return createRequestContextWithDataSourceId(dataSourceId).httpGet({ + http, + url: API_ENDPOINT_ROLES, + }); } // TODO: have a type definition for it -export function fetchRoleMapping(http: HttpStart, query: HttpFetchQuery): Promise { - return httpGet({ http, url: API_ENDPOINT_ROLESMAPPING, query }); +export function fetchRoleMapping(http: HttpStart, dataSourceId: string): Promise { + return createRequestContextWithDataSourceId(dataSourceId).httpGet({ + http, + url: API_ENDPOINT_ROLESMAPPING, + }); } diff --git a/public/apps/configuration/utils/role-mapping-utils.tsx b/public/apps/configuration/utils/role-mapping-utils.tsx index 856f7c685..6f34d092f 100644 --- a/public/apps/configuration/utils/role-mapping-utils.tsx +++ b/public/apps/configuration/utils/role-mapping-utils.tsx @@ -14,10 +14,10 @@ */ import { map } from 'lodash'; -import { HttpFetchQuery, HttpStart } from '../../../../../../src/core/public'; +import { HttpStart } from '../../../../../../src/core/public'; import { API_ENDPOINT_ROLESMAPPING } from '../constants'; import { RoleMappingDetail } from '../types'; -import { httpGetWithIgnores, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; export interface MappedUsersListing { @@ -30,12 +30,11 @@ export enum UserType { external = 'Backend role', } -export async function getRoleMappingData(http: HttpStart, roleName: string, query: HttpFetchQuery) { - return httpGetWithIgnores({ +export async function getRoleMappingData(http: HttpStart, roleName: string, dataSourceId: string) { + return createRequestContextWithDataSourceId(dataSourceId).httpGetWithIgnores({ http, url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), ignores: [404], - query, }); } @@ -57,12 +56,11 @@ export async function updateRoleMapping( http: HttpStart, roleName: string, updateObject: RoleMappingDetail, - query: HttpFetchQuery + dataSourceId: string ) { - return await httpPost({ + return await createRequestContextWithDataSourceId(dataSourceId).httpPost({ http, url: getResourceUrl(API_ENDPOINT_ROLESMAPPING, roleName), body: updateObject, - query, }); } diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index adff9d165..6dd1d315d 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -15,14 +15,14 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_TENANCY_CONFIGS } from '../constants'; -import { httpGet, httpPost } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; - -export async function updateTenancyConfig(http: HttpStart, updateObject: TenancyConfigSettings) { - return await httpPost({ http, url: API_ENDPOINT_TENANCY_CONFIGS, body: updateObject }); -} +import { LocalClusterId } from '../../../../common'; export async function getTenancyConfig(http: HttpStart): Promise { - const rawConfiguration = await httpGet({ http, url: API_ENDPOINT_TENANCY_CONFIGS }); + const rawConfiguration = await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_TENANCY_CONFIGS, + }); return rawConfiguration; } diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index c467c809e..b5cbd1100 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { HttpFetchQuery, HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart } from 'opensearch-dashboards/public'; import { map } from 'lodash'; import React from 'react'; import { i18n } from '@osd/i18n'; @@ -36,7 +36,7 @@ import { TenantSelect, TenantUpdate, } from '../types'; -import { httpDelete, httpGet, httpPost, httpPut } from './request-utils'; +import { createRequestContextWithDataSourceId } from './request-utils'; import { getResourceUrl } from './resource-utils'; import { DEFAULT_TENANT, @@ -45,6 +45,7 @@ import { globalTenantName, isGlobalTenant, isRenderingPrivateTenant, + LocalClusterId, PRIVATE_TENANT_RENDERING_TEXT, } from '../../../../common'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; @@ -63,16 +64,21 @@ export const PRIVATE_USER_DICT: { [key: string]: string } = { export async function fetchTenants( http: HttpStart, - query: HttpFetchQuery + dataSourceId: string ): Promise> { - return (await httpGet>({ http, url: API_ENDPOINT_TENANTS, query })).data; + return ( + await createRequestContextWithDataSourceId(dataSourceId).httpGet>({ + http, + url: API_ENDPOINT_TENANTS, + }) + ).data; } export async function fetchTenantNameList( http: HttpStart, - query: HttpFetchQuery + dataSourceId: string ): Promise { - return Object.keys(await fetchTenants(http, query)); + return Object.keys(await fetchTenants(http, dataSourceId)); } export function transformTenantData(rawTenantData: DataObject): Tenant[] { @@ -93,7 +99,10 @@ export function transformTenantData(rawTenantData: DataObject): Tenant[] } export async function fetchCurrentTenant(http: HttpStart): Promise { - return await httpGet({ http, url: API_ENDPOINT_MULTITENANCY }); + return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_MULTITENANCY, + }); } export async function updateTenant( @@ -101,7 +110,7 @@ export async function updateTenant( tenantName: string, updateObject: TenantUpdate ) { - return await httpPost({ + return await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenantName), body: updateObject, @@ -112,19 +121,30 @@ export async function updateTenancyConfiguration( http: HttpStart, updatedTenancyConfig: TenancyConfigSettings ) { - await httpPut(http, API_ENDPOINT_TENANCY_CONFIGS, updatedTenancyConfig); - + // Tenancy locked to local cluster + await createRequestContextWithDataSourceId(LocalClusterId).httpPut( + http, + API_ENDPOINT_TENANCY_CONFIGS, + updatedTenancyConfig + ); return; } export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { for (const tenant of tenants) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenant) }); + await createRequestContextWithDataSourceId(LocalClusterId).httpDelete({ + http, + url: getResourceUrl(API_ENDPOINT_TENANTS, tenant), + }); } } export async function selectTenant(http: HttpStart, selectObject: TenantSelect): Promise { - return await httpPost({ http, url: API_ENDPOINT_MULTITENANCY, body: selectObject }); + return await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + http, + url: API_ENDPOINT_MULTITENANCY, + body: selectObject, + }); } export const RESOLVED_GLOBAL_TENANT = 'Global'; diff --git a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx index 74f8e8c64..7a0a819a4 100644 --- a/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx +++ b/public/apps/configuration/utils/test/internal-user-list-utils.test.tsx @@ -14,11 +14,14 @@ */ import { fetchUserNameList, getUserList, transformUserData } from '../internal-user-list-utils'; -import { httpGet } from '../request-utils'; -import * as InternalUserListUtils from '../internal-user-list-utils'; +// Import RequestContext + +const mockedHttpGet = jest.fn().mockResolvedValue({ data: {} }); jest.mock('../../utils/request-utils', () => ({ - httpGet: jest.fn().mockResolvedValue({ data: {} }), + createRequestContextWithDataSourceId: jest.fn(() => ({ + httpGet: mockedHttpGet, + })), })); describe('Internal user list utils', () => { @@ -42,20 +45,12 @@ describe('Internal user list utils', () => { it('getUserList calls httpGet with the correct parameters for internal users', async () => { const httpMock = {}; // Mock HttpStart object const userType = 'internalaccounts'; - const query = { dataSourceId: 'test' }; - - const mockRawData = { - data: {}, - }; - - jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - const test = await getUserList(httpMock, userType, query); + const test = await getUserList(httpMock, userType, 'test'); - expect(httpGet).toHaveBeenCalledWith({ + expect(mockedHttpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/internalaccounts', - query, }); expect(test).toEqual([]); }); @@ -63,20 +58,12 @@ describe('Internal user list utils', () => { it('getUserList calls httpGet with the correct parameters for service accounts', async () => { const httpMock = {}; const userType = 'serviceAccounts'; - const query = { dataSourceId: 'test' }; - - const mockRawData = { - data: {}, - }; - - jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - const test = await getUserList(httpMock, userType, query); + const test = await getUserList(httpMock, userType, 'test'); - expect(httpGet).toHaveBeenCalledWith({ + expect(mockedHttpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/serviceaccounts', - query, }); expect(test).toEqual([]); }); @@ -85,15 +72,9 @@ describe('Internal user list utils', () => { const httpMock = {}; const userType = 'serviceAccounts'; - const mockRawData = { - data: {}, - }; + const test = await fetchUserNameList(httpMock, userType, ''); - jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - - const test = await fetchUserNameList(httpMock, userType); - - expect(httpGet).toHaveBeenCalledWith({ + expect(mockedHttpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/serviceaccounts', }); @@ -104,15 +85,9 @@ describe('Internal user list utils', () => { const httpMock = {}; const userType = 'internalaccounts'; - const mockRawData = { - data: {}, - }; - - jest.spyOn(InternalUserListUtils, 'getUserListRaw').mockResolvedValue(mockRawData); - - const test = await fetchUserNameList(httpMock, userType); + const test = await fetchUserNameList(httpMock, userType, ''); - expect(httpGet).toHaveBeenCalledWith({ + expect(mockedHttpGet).toHaveBeenCalledWith({ http: httpMock, url: '/api/v1/configuration/internalaccounts', }); diff --git a/public/apps/configuration/utils/test/request-utils.test.ts b/public/apps/configuration/utils/test/request-utils.test.ts new file mode 100644 index 000000000..02d508b2f --- /dev/null +++ b/public/apps/configuration/utils/test/request-utils.test.ts @@ -0,0 +1,45 @@ +/* + * 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 * as requestUtils from '../request-utils'; +import { HttpStart } from 'opensearch-dashboards/public'; + +describe('RequestContext', () => { + let httpMock: HttpStart; + + beforeEach(() => { + // Mocking HttpStart + httpMock = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should error if no dataSourceId is passed', () => { + expect(() => createRequestContextWithDataSourceId()).toThrowError(); + }); + + it('should have the correct query based on the passed dataSourceId', () => { + const context = requestUtils.createRequestContextWithDataSourceId('test'); + expect(context.query).toEqual({ dataSourceId: 'test' }); + }); + + // You can write similar tests for other methods like httpPost, httpPut, httpDelete, etc. +}); diff --git a/public/utils/auth-info-utils.tsx b/public/utils/auth-info-utils.tsx index c766c7bce..97c2c0a44 100644 --- a/public/utils/auth-info-utils.tsx +++ b/public/utils/auth-info-utils.tsx @@ -14,12 +14,15 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_AUTHINFO } from '../../common'; -import { httpGet } from '../apps/configuration/utils/request-utils'; +import { API_ENDPOINT_AUTHINFO, LocalClusterId } from '../../common'; +import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { AuthInfo } from '../types'; export async function getAuthInfo(http: HttpStart) { - return await httpGet({ http, url: API_ENDPOINT_AUTHINFO }); + return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_AUTHINFO, + }); } export async function getCurrentUser(http: HttpStart) { diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index 9c636ee47..94d300532 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -14,16 +14,19 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_DASHBOARDSINFO } from '../../common'; -import { httpGet, httpGetWithIgnores } from '../apps/configuration/utils/request-utils'; +import { API_ENDPOINT_DASHBOARDSINFO, LocalClusterId } from '../../common'; +import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { DashboardsInfo } from '../types'; export async function getDashboardsInfo(http: HttpStart) { - return await httpGet({ http, url: API_ENDPOINT_DASHBOARDSINFO }); + return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_DASHBOARDSINFO, + }); } export async function getDashboardsInfoSafe(http: HttpStart): Promise { - return httpGetWithIgnores({ + return createRequestContextWithDataSourceId(LocalClusterId).httpGetWithIgnores({ http, url: API_ENDPOINT_DASHBOARDSINFO, ignores: [401], diff --git a/public/utils/login-utils.tsx b/public/utils/login-utils.tsx index 7c0d98bec..00dea9a08 100644 --- a/public/utils/login-utils.tsx +++ b/public/utils/login-utils.tsx @@ -14,14 +14,15 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { httpPost } from '../apps/configuration/utils/request-utils'; +import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; +import { LocalClusterId } from '../../common'; export async function validateCurrentPassword( http: HttpStart, userName: string, currentPassword: string ): Promise { - await httpPost({ + await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ http, url: '/auth/login', body: { diff --git a/public/utils/logout-utils.tsx b/public/utils/logout-utils.tsx index d195cc79f..0e2c1978a 100644 --- a/public/utils/logout-utils.tsx +++ b/public/utils/logout-utils.tsx @@ -18,8 +18,13 @@ import { HttpStart, IHttpInterceptController, } from '../../../../src/core/public'; -import { API_ENDPOINT_AUTHTYPE, CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI } from '../../common'; -import { httpGet } from '../apps/configuration/utils/request-utils'; +import { + API_ENDPOINT_AUTHTYPE, + CUSTOM_ERROR_PAGE_URI, + LOGIN_PAGE_URI, + LocalClusterId, +} from '../../common'; +import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { setShouldShowTenantPopup } from './storage-utils'; export function interceptError(logoutUrl: string, thisWindow: Window): any { @@ -47,5 +52,8 @@ export function interceptError(logoutUrl: string, thisWindow: Window): any { } export async function fetchCurrentAuthType(http: HttpStart): Promise { - return await httpGet({ http, url: API_ENDPOINT_AUTHTYPE }); + return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + http, + url: API_ENDPOINT_AUTHTYPE, + }); } diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index cdde2df63..60f32d02f 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -const externalTitle = '9202'; const createDataSource = () => { return cy.request({ method: 'POST', @@ -23,13 +22,13 @@ const createDataSource = () => { }, body: { attributes: { - title: externalTitle, - endpoint: 'https://localhost:9202', + title: Cypress.env('externalDataSourceLabel'), + endpoint: Cypress.env('externalDataSourceEndpoint'), auth: { type: 'username_password', credentials: { - username: 'admin', - password: 'myStrongPassword123!', + username: Cypress.env('externalDataSourceAdminUserName'), + password: Cypress.env('externalDataSourceAdminPassword'), }, }, }, @@ -76,7 +75,10 @@ describe('Multi-datasources enabled', () => { if (resp && resp.body) { externalDataSourceId = resp.body.id; } - externalDataSourceUrl = createUrlParam(externalTitle, externalDataSourceId); + externalDataSourceUrl = createUrlParam( + Cypress.env('externalDataSourceLabel'), + externalDataSourceId + ); localDataSourceUrl = createUrlParam('Local cluster', ''); }); }); @@ -122,7 +124,7 @@ describe('Multi-datasources enabled', () => { body: { backend_roles: [''], attributes: {}, - password: 'myStrongPassword123!', + password: 'myStrongPassword12345678!', }, }).then(() => { cy.visit( From c048a430440bf2131ba0eb19ce63e0df39eb12a5 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 25 Apr 2024 12:58:32 -0400 Subject: [PATCH 18/22] Address feedback Signed-off-by: Derek Ho --- common/index.ts | 2 +- public/apps/account/utils.tsx | 10 ++-- .../panels/service-account-list.tsx | 4 +- .../panels/tenant-list/configure_tab1.tsx | 4 +- .../panels/tenant-list/manage_tab.tsx | 4 +- .../apps/configuration/utils/request-utils.ts | 52 +++++++++++-------- .../utils/tenancy-config_util.tsx | 6 ++- .../apps/configuration/utils/tenant-utils.tsx | 12 ++--- public/utils/auth-info-utils.tsx | 4 +- public/utils/dashboards-info-utils.tsx | 6 +-- public/utils/login-utils.tsx | 4 +- public/utils/logout-utils.tsx | 4 +- 12 files changed, 62 insertions(+), 50 deletions(-) diff --git a/common/index.ts b/common/index.ts index 66abaa09f..84445486d 100644 --- a/common/index.ts +++ b/common/index.ts @@ -56,7 +56,7 @@ export const MAX_INTEGER = 2147483647; export const MAX_LENGTH_OF_COOKIE_BYTES = 4000; export const ESTIMATED_IRON_COOKIE_OVERHEAD = 1.5; -export const LocalClusterId = ''; +export const LOCAL_CLUSTER_ID = ''; export enum AuthType { BASIC = 'basicauth', diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index 0c82f9e6f..a701f3d1c 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -14,21 +14,21 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_AUTH_LOGOUT, LocalClusterId } from '../../../common'; +import { API_AUTH_LOGOUT, LOCAL_CLUSTER_ID } from '../../../common'; import { setShouldShowTenantPopup } from '../../utils/storage-utils'; import { createRequestContextWithDataSourceId } from '../configuration/utils/request-utils'; import { API_ENDPOINT_ACCOUNT_INFO } from './constants'; import { AccountInfo } from './types'; export function fetchAccountInfo(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ http, url: API_ENDPOINT_ACCOUNT_INFO, }); } export async function fetchAccountInfoSafe(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LocalClusterId).httpGetWithIgnores({ + return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGetWithIgnores({ http, url: API_ENDPOINT_ACCOUNT_INFO, ignores: [401], @@ -36,7 +36,7 @@ export async function fetchAccountInfoSafe(http: HttpStart): Promise { - await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ http, url: API_AUTH_LOGOUT, }); @@ -62,7 +62,7 @@ export async function updateNewPassword( newPassword: string, currentPassword: string ): Promise { - await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ http, url: API_ENDPOINT_ACCOUNT_INFO, body: { diff --git a/public/apps/configuration/panels/service-account-list.tsx b/public/apps/configuration/panels/service-account-list.tsx index 6ce2e74f3..4ed2d7a5f 100644 --- a/public/apps/configuration/panels/service-account-list.tsx +++ b/public/apps/configuration/panels/service-account-list.tsx @@ -35,7 +35,7 @@ import { getAuthInfo } from '../../../utils/auth-info-utils'; import { AppDependencies } from '../../types'; import { API_ENDPOINT_SERVICEACCOUNTS, DocLinks } from '../constants'; import { Action } from '../types'; -import { LocalClusterId, ResourceType } from '../../../../common'; +import { LOCAL_CLUSTER_ID, ResourceType } from '../../../../common'; import { EMPTY_FIELD_VALUE } from '../ui-constants'; import { useContextMenuState } from '../utils/context-menu'; import { ExternalLink, tableItemsUIProps, truncatedListView } from '../utils/display-utils'; @@ -107,7 +107,7 @@ export function ServiceAccountList(props: AppDependencies) { const userDataPromise = getUserList( props.coreStart.http, ResourceType.serviceAccounts, - LocalClusterId + LOCAL_CLUSTER_ID ); setCurrentUsername((await getAuthInfo(props.coreStart.http)).user_name); setUserData(await userDataPromise); diff --git a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx index 2afa65580..2430ce1ac 100644 --- a/public/apps/configuration/panels/tenant-list/configure_tab1.tsx +++ b/public/apps/configuration/panels/tenant-list/configure_tab1.tsx @@ -49,7 +49,7 @@ import { useToastState, } from '../../utils/toast-utils'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; -import { LocalClusterId } from '../../../../../common'; +import { LOCAL_CLUSTER_ID } from '../../../../../common'; export function ConfigureTab1(props: AppDependencies) { const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(false); @@ -192,7 +192,7 @@ export function ConfigureTab1(props: AppDependencies) { default_tenant: (await getDashboardsInfo(props.coreStart.http)).default_tenant, }); - const rawTenantData = await fetchTenants(props.coreStart.http, LocalClusterId); + const rawTenantData = await fetchTenants(props.coreStart.http, LOCAL_CLUSTER_ID); const processedTenantData = transformTenantData(rawTenantData); setTenantData(processedTenantData); } catch (e) { diff --git a/public/apps/configuration/panels/tenant-list/manage_tab.tsx b/public/apps/configuration/panels/tenant-list/manage_tab.tsx index 8031b3ce4..79bcbe908 100644 --- a/public/apps/configuration/panels/tenant-list/manage_tab.tsx +++ b/public/apps/configuration/panels/tenant-list/manage_tab.tsx @@ -42,7 +42,7 @@ import { flow } from 'lodash'; import { getCurrentUser } from '../../../../utils/auth-info-utils'; import { AppDependencies } from '../../../types'; import { Action, Tenant } from '../../types'; -import { LocalClusterId, ResourceType } from '../../../../../common'; +import { LOCAL_CLUSTER_ID, ResourceType } from '../../../../../common'; import { ExternalLink, renderCustomization, tableItemsUIProps } from '../../utils/display-utils'; import { fetchTenants, @@ -97,7 +97,7 @@ export function ManageTab(props: AppDependencies) { const fetchData = useCallback(async () => { try { setLoading(true); - const rawTenantData = await fetchTenants(http, LocalClusterId); + const rawTenantData = await fetchTenants(http, LOCAL_CLUSTER_ID); const processedTenantData = transformTenantData(rawTenantData); const activeTenant = await fetchCurrentTenant(http); const currentUser = await getCurrentUser(http); diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index 419e4f4d6..c2dd1c17c 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -15,16 +15,29 @@ import { HttpStart, HttpHandler, HttpFetchQuery } from 'opensearch-dashboards/public'; -interface RequestType { - http: HttpStart; +interface BaseRequestParams { url: string; body?: object; } -interface RequestParams { +interface BaseRequestParamsWithIgnores extends BaseRequestParams { + ignores: number[]; +} + +interface CreateRequestParams extends BaseRequestParams { + http: HttpStart; +} + +interface ExecuteRequestParams extends BaseRequestParams { + requestFunc: HttpHandler; + query: HttpFetchQuery; +} + +interface CreateRequestWithIgnoreParams extends BaseRequestParamsWithIgnores { + http: HttpStart; +} +interface ExecuteRequestWithIgnoreParams extends BaseRequestParamsWithIgnores { requestFunc: HttpHandler; - url: string; - body?: object; query: HttpFetchQuery; } @@ -43,26 +56,29 @@ export class RequestContext { }; } - public async httpGet(params: RequestType): Promise { + public async httpGet(params: CreateRequestParams): Promise { const { http, url, body } = params; return await request({ requestFunc: http.get, url, body, query: this.query }); } - public async httpPost(params: RequestType): Promise { + public async httpPost(params: CreateRequestParams): Promise { const { http, url, body } = params; return await request({ requestFunc: http.post, url, body, query: this.query }); } - public async httpPut(http: HttpStart, url: string, body?: object): Promise { + public async httpPut(params: CreateRequestParams): Promise { + const { http, url, body } = params; return await request({ requestFunc: http.put, url, body, query: this.query }); } - public async httpDelete(params: RequestType): Promise { + public async httpDelete(params: CreateRequestParams): Promise { const { http, url, body } = params; return await request({ requestFunc: http.delete, url, body, query: this.query }); } - public async httpDeleteWithIgnores(params: RequestTypeWithIgnore): Promise { + public async httpDeleteWithIgnores( + params: CreateRequestWithIgnoreParams + ): Promise { const { http, url, ignores } = params; return await requestWithIgnores({ requestFunc: http.delete, @@ -72,14 +88,15 @@ export class RequestContext { }); } - public async httpGetWithIgnores(params: RequestTypeWithIgnore): Promise { + public async httpGetWithIgnores( + params: CreateRequestWithIgnoreParams + ): Promise { const { http, url, ignores } = params; return await requestWithIgnores({ requestFunc: http.get, url, ignores, query: this.query }); } } -export async function request(params: RequestParams): Promise { - console.log(params); +export async function request(params: ExecuteRequestParams): Promise { const { requestFunc, url, body, query } = params; if (body) { return (await requestFunc(url, { body: JSON.stringify(body), query })) as T; @@ -87,13 +104,6 @@ export async function request(params: RequestParams): Promise { return (await requestFunc(url, { query })) as T; } -interface RequestTypeWithIgnore extends RequestType { - ignores: number[]; -} -interface RequestParamsWithIgnore extends RequestParams { - ignores: number[]; -} - /** * Send a request but ignore some error codes (suppress exception) * @param requestFunc @@ -101,7 +111,7 @@ interface RequestParamsWithIgnore extends RequestParams { * @param ignores the error codes to be ignored */ export async function requestWithIgnores( - params: RequestParamsWithIgnore + params: ExecuteRequestWithIgnoreParams ): Promise { const { requestFunc, url, ignores, body, query } = params; try { diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index 6dd1d315d..3d7075c19 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -17,10 +17,12 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_TENANCY_CONFIGS } from '../constants'; import { createRequestContextWithDataSourceId } from './request-utils'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; -import { LocalClusterId } from '../../../../common'; +import { LOCAL_CLUSTER_ID } from '../../../../common'; export async function getTenancyConfig(http: HttpStart): Promise { - const rawConfiguration = await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + const rawConfiguration = await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet< + any + >({ http, url: API_ENDPOINT_TENANCY_CONFIGS, }); diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index b5cbd1100..e2b6f7619 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -45,7 +45,7 @@ import { globalTenantName, isGlobalTenant, isRenderingPrivateTenant, - LocalClusterId, + LOCAL_CLUSTER_ID, PRIVATE_TENANT_RENDERING_TEXT, } from '../../../../common'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; @@ -99,7 +99,7 @@ export function transformTenantData(rawTenantData: DataObject): Tenant[] } export async function fetchCurrentTenant(http: HttpStart): Promise { - return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ http, url: API_ENDPOINT_MULTITENANCY, }); @@ -110,7 +110,7 @@ export async function updateTenant( tenantName: string, updateObject: TenantUpdate ) { - return await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenantName), body: updateObject, @@ -122,7 +122,7 @@ export async function updateTenancyConfiguration( updatedTenancyConfig: TenancyConfigSettings ) { // Tenancy locked to local cluster - await createRequestContextWithDataSourceId(LocalClusterId).httpPut( + await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPut( http, API_ENDPOINT_TENANCY_CONFIGS, updatedTenancyConfig @@ -132,7 +132,7 @@ export async function updateTenancyConfiguration( export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { for (const tenant of tenants) { - await createRequestContextWithDataSourceId(LocalClusterId).httpDelete({ + await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpDelete({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenant), }); @@ -140,7 +140,7 @@ export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { } export async function selectTenant(http: HttpStart, selectObject: TenantSelect): Promise { - return await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ http, url: API_ENDPOINT_MULTITENANCY, body: selectObject, diff --git a/public/utils/auth-info-utils.tsx b/public/utils/auth-info-utils.tsx index 97c2c0a44..037c50a8d 100644 --- a/public/utils/auth-info-utils.tsx +++ b/public/utils/auth-info-utils.tsx @@ -14,12 +14,12 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_AUTHINFO, LocalClusterId } from '../../common'; +import { API_ENDPOINT_AUTHINFO, LOCAL_CLUSTER_ID } from '../../common'; import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { AuthInfo } from '../types'; export async function getAuthInfo(http: HttpStart) { - return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ http, url: API_ENDPOINT_AUTHINFO, }); diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index 94d300532..be6c9f7f4 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -14,19 +14,19 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_DASHBOARDSINFO, LocalClusterId } from '../../common'; +import { API_ENDPOINT_DASHBOARDSINFO, LOCAL_CLUSTER_ID } from '../../common'; import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { DashboardsInfo } from '../types'; export async function getDashboardsInfo(http: HttpStart) { - return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ http, url: API_ENDPOINT_DASHBOARDSINFO, }); } export async function getDashboardsInfoSafe(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LocalClusterId).httpGetWithIgnores({ + return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGetWithIgnores({ http, url: API_ENDPOINT_DASHBOARDSINFO, ignores: [401], diff --git a/public/utils/login-utils.tsx b/public/utils/login-utils.tsx index 00dea9a08..52a288a89 100644 --- a/public/utils/login-utils.tsx +++ b/public/utils/login-utils.tsx @@ -15,14 +15,14 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; -import { LocalClusterId } from '../../common'; +import { LOCAL_CLUSTER_ID } from '../../common'; export async function validateCurrentPassword( http: HttpStart, userName: string, currentPassword: string ): Promise { - await createRequestContextWithDataSourceId(LocalClusterId).httpPost({ + await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ http, url: '/auth/login', body: { diff --git a/public/utils/logout-utils.tsx b/public/utils/logout-utils.tsx index 0e2c1978a..3dbb1e261 100644 --- a/public/utils/logout-utils.tsx +++ b/public/utils/logout-utils.tsx @@ -22,7 +22,7 @@ import { API_ENDPOINT_AUTHTYPE, CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI, - LocalClusterId, + LOCAL_CLUSTER_ID, } from '../../common'; import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; import { setShouldShowTenantPopup } from './storage-utils'; @@ -52,7 +52,7 @@ export function interceptError(logoutUrl: string, thisWindow: Window): any { } export async function fetchCurrentAuthType(http: HttpStart): Promise { - return await createRequestContextWithDataSourceId(LocalClusterId).httpGet({ + return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ http, url: API_ENDPOINT_AUTHTYPE, }); From 42feb3d157411a0f610696dbe83f7a4276be2f8e Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 25 Apr 2024 16:02:33 -0400 Subject: [PATCH 19/22] Udpate usages to create a local cluster context Signed-off-by: Derek Ho --- public/apps/account/utils.tsx | 12 ++++++------ public/apps/configuration/utils/request-utils.ts | 5 +++++ .../configuration/utils/tenancy-config_util.tsx | 7 ++----- public/apps/configuration/utils/tenant-utils.tsx | 16 +++++++++------- .../utils/test/request-utils.test.ts | 5 +++++ public/utils/auth-info-utils.tsx | 6 +++--- public/utils/dashboards-info-utils.tsx | 8 ++++---- public/utils/login-utils.tsx | 5 ++--- public/utils/logout-utils.tsx | 11 +++-------- 9 files changed, 39 insertions(+), 36 deletions(-) diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index a701f3d1c..ab41bd34a 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -14,21 +14,21 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_AUTH_LOGOUT, LOCAL_CLUSTER_ID } from '../../../common'; +import { API_AUTH_LOGOUT } from '../../../common'; import { setShouldShowTenantPopup } from '../../utils/storage-utils'; -import { createRequestContextWithDataSourceId } from '../configuration/utils/request-utils'; import { API_ENDPOINT_ACCOUNT_INFO } from './constants'; import { AccountInfo } from './types'; +import { createLocalClusterRequestContext } from '../configuration/utils/request-utils'; export function fetchAccountInfo(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ + return createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_ACCOUNT_INFO, }); } export async function fetchAccountInfoSafe(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGetWithIgnores({ + return createLocalClusterRequestContext().httpGetWithIgnores({ http, url: API_ENDPOINT_ACCOUNT_INFO, ignores: [401], @@ -36,7 +36,7 @@ export async function fetchAccountInfoSafe(http: HttpStart): Promise { - await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ + await createLocalClusterRequestContext().httpPost({ http, url: API_AUTH_LOGOUT, }); @@ -62,7 +62,7 @@ export async function updateNewPassword( newPassword: string, currentPassword: string ): Promise { - await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ + await createLocalClusterRequestContext().httpPost({ http, url: API_ENDPOINT_ACCOUNT_INFO, body: { diff --git a/public/apps/configuration/utils/request-utils.ts b/public/apps/configuration/utils/request-utils.ts index c2dd1c17c..2770a678d 100644 --- a/public/apps/configuration/utils/request-utils.ts +++ b/public/apps/configuration/utils/request-utils.ts @@ -14,6 +14,7 @@ */ import { HttpStart, HttpHandler, HttpFetchQuery } from 'opensearch-dashboards/public'; +import { LOCAL_CLUSTER_ID } from '../../../../common'; interface BaseRequestParams { url: string; @@ -48,6 +49,10 @@ export function createRequestContextWithDataSourceId(dataSourceId: string) { return new RequestContext(dataSourceId); } +export function createLocalClusterRequestContext() { + return new RequestContext(LOCAL_CLUSTER_ID); +} + export class RequestContext { query: HttpFetchQuery; constructor(private readonly dataSourceId: string) { diff --git a/public/apps/configuration/utils/tenancy-config_util.tsx b/public/apps/configuration/utils/tenancy-config_util.tsx index 3d7075c19..0c9eff04a 100644 --- a/public/apps/configuration/utils/tenancy-config_util.tsx +++ b/public/apps/configuration/utils/tenancy-config_util.tsx @@ -15,14 +15,11 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_TENANCY_CONFIGS } from '../constants'; -import { createRequestContextWithDataSourceId } from './request-utils'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; -import { LOCAL_CLUSTER_ID } from '../../../../common'; +import { createLocalClusterRequestContext } from './request-utils'; export async function getTenancyConfig(http: HttpStart): Promise { - const rawConfiguration = await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet< - any - >({ + const rawConfiguration = await createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_TENANCY_CONFIGS, }); diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index e2b6f7619..1ae8821e7 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -36,7 +36,10 @@ import { TenantSelect, TenantUpdate, } from '../types'; -import { createRequestContextWithDataSourceId } from './request-utils'; +import { + createLocalClusterRequestContext, + createRequestContextWithDataSourceId, +} from './request-utils'; import { getResourceUrl } from './resource-utils'; import { DEFAULT_TENANT, @@ -45,7 +48,6 @@ import { globalTenantName, isGlobalTenant, isRenderingPrivateTenant, - LOCAL_CLUSTER_ID, PRIVATE_TENANT_RENDERING_TEXT, } from '../../../../common'; import { TenancyConfigSettings } from '../panels/tenancy-config/types'; @@ -99,7 +101,7 @@ export function transformTenantData(rawTenantData: DataObject): Tenant[] } export async function fetchCurrentTenant(http: HttpStart): Promise { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ + return await createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_MULTITENANCY, }); @@ -110,7 +112,7 @@ export async function updateTenant( tenantName: string, updateObject: TenantUpdate ) { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ + return await createLocalClusterRequestContext().httpPost({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenantName), body: updateObject, @@ -122,7 +124,7 @@ export async function updateTenancyConfiguration( updatedTenancyConfig: TenancyConfigSettings ) { // Tenancy locked to local cluster - await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPut( + await createLocalClusterRequestContext().httpPut( http, API_ENDPOINT_TENANCY_CONFIGS, updatedTenancyConfig @@ -132,7 +134,7 @@ export async function updateTenancyConfiguration( export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { for (const tenant of tenants) { - await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpDelete({ + await createLocalClusterRequestContext().httpDelete({ http, url: getResourceUrl(API_ENDPOINT_TENANTS, tenant), }); @@ -140,7 +142,7 @@ export async function requestDeleteTenant(http: HttpStart, tenants: string[]) { } export async function selectTenant(http: HttpStart, selectObject: TenantSelect): Promise { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ + return await createLocalClusterRequestContext().httpPost({ http, url: API_ENDPOINT_MULTITENANCY, body: selectObject, diff --git a/public/apps/configuration/utils/test/request-utils.test.ts b/public/apps/configuration/utils/test/request-utils.test.ts index 02d508b2f..564e155ea 100644 --- a/public/apps/configuration/utils/test/request-utils.test.ts +++ b/public/apps/configuration/utils/test/request-utils.test.ts @@ -41,5 +41,10 @@ describe('RequestContext', () => { expect(context.query).toEqual({ dataSourceId: 'test' }); }); + it('should have the correct query based on local cluster context', () => { + const context = requestUtils.createLocalClusterRequestContext(); + expect(context.query).toEqual({ dataSourceId: '' }); + }); + // You can write similar tests for other methods like httpPost, httpPut, httpDelete, etc. }); diff --git a/public/utils/auth-info-utils.tsx b/public/utils/auth-info-utils.tsx index 037c50a8d..eaa3292a8 100644 --- a/public/utils/auth-info-utils.tsx +++ b/public/utils/auth-info-utils.tsx @@ -14,12 +14,12 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_AUTHINFO, LOCAL_CLUSTER_ID } from '../../common'; -import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; +import { API_ENDPOINT_AUTHINFO } from '../../common'; import { AuthInfo } from '../types'; +import { createLocalClusterRequestContext } from '../apps/configuration/utils/request-utils'; export async function getAuthInfo(http: HttpStart) { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ + return await createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_AUTHINFO, }); diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index be6c9f7f4..400e4da1b 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -14,19 +14,19 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_DASHBOARDSINFO, LOCAL_CLUSTER_ID } from '../../common'; -import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; +import { API_ENDPOINT_DASHBOARDSINFO } from '../../common'; import { DashboardsInfo } from '../types'; +import { createLocalClusterRequestContext } from '../apps/configuration/utils/request-utils'; export async function getDashboardsInfo(http: HttpStart) { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ + return await createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_DASHBOARDSINFO, }); } export async function getDashboardsInfoSafe(http: HttpStart): Promise { - return createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGetWithIgnores({ + return createLocalClusterRequestContext().httpGetWithIgnores({ http, url: API_ENDPOINT_DASHBOARDSINFO, ignores: [401], diff --git a/public/utils/login-utils.tsx b/public/utils/login-utils.tsx index 52a288a89..85d63e354 100644 --- a/public/utils/login-utils.tsx +++ b/public/utils/login-utils.tsx @@ -14,15 +14,14 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; -import { LOCAL_CLUSTER_ID } from '../../common'; +import { createLocalClusterRequestContext } from '../apps/configuration/utils/request-utils'; export async function validateCurrentPassword( http: HttpStart, userName: string, currentPassword: string ): Promise { - await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpPost({ + await createLocalClusterRequestContext().httpPost({ http, url: '/auth/login', body: { diff --git a/public/utils/logout-utils.tsx b/public/utils/logout-utils.tsx index 3dbb1e261..7ab38e459 100644 --- a/public/utils/logout-utils.tsx +++ b/public/utils/logout-utils.tsx @@ -18,13 +18,8 @@ import { HttpStart, IHttpInterceptController, } from '../../../../src/core/public'; -import { - API_ENDPOINT_AUTHTYPE, - CUSTOM_ERROR_PAGE_URI, - LOGIN_PAGE_URI, - LOCAL_CLUSTER_ID, -} from '../../common'; -import { createRequestContextWithDataSourceId } from '../apps/configuration/utils/request-utils'; +import { API_ENDPOINT_AUTHTYPE, CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI } from '../../common'; +import { createLocalClusterRequestContext } from '../apps/configuration/utils/request-utils'; import { setShouldShowTenantPopup } from './storage-utils'; export function interceptError(logoutUrl: string, thisWindow: Window): any { @@ -52,7 +47,7 @@ export function interceptError(logoutUrl: string, thisWindow: Window): any { } export async function fetchCurrentAuthType(http: HttpStart): Promise { - return await createRequestContextWithDataSourceId(LOCAL_CLUSTER_ID).httpGet({ + return await createLocalClusterRequestContext().httpGet({ http, url: API_ENDPOINT_AUTHTYPE, }); From 3be7bb57aedd2471b180d51965e73cf3304502cc Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 26 Apr 2024 09:45:24 -0400 Subject: [PATCH 20/22] Fix cypress tests Signed-off-by: Derek Ho --- public/apps/configuration/utils/tenant-utils.tsx | 8 ++++---- .../configuration/utils/test/request-utils.test.ts | 2 -- .../multi_datasources_enabled.spec.js | 10 ++-------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/public/apps/configuration/utils/tenant-utils.tsx b/public/apps/configuration/utils/tenant-utils.tsx index 1ae8821e7..b1df275de 100644 --- a/public/apps/configuration/utils/tenant-utils.tsx +++ b/public/apps/configuration/utils/tenant-utils.tsx @@ -124,11 +124,11 @@ export async function updateTenancyConfiguration( updatedTenancyConfig: TenancyConfigSettings ) { // Tenancy locked to local cluster - await createLocalClusterRequestContext().httpPut( + await createLocalClusterRequestContext().httpPut({ http, - API_ENDPOINT_TENANCY_CONFIGS, - updatedTenancyConfig - ); + url: API_ENDPOINT_TENANCY_CONFIGS, + body: updatedTenancyConfig, + }); return; } diff --git a/public/apps/configuration/utils/test/request-utils.test.ts b/public/apps/configuration/utils/test/request-utils.test.ts index 564e155ea..d75b64a71 100644 --- a/public/apps/configuration/utils/test/request-utils.test.ts +++ b/public/apps/configuration/utils/test/request-utils.test.ts @@ -45,6 +45,4 @@ describe('RequestContext', () => { const context = requestUtils.createLocalClusterRequestContext(); expect(context.query).toEqual({ dataSourceId: '' }); }); - - // You can write similar tests for other methods like httpPost, httpPut, httpDelete, etc. }); diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 60f32d02f..2a1a0c4f7 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -97,10 +97,7 @@ describe('Multi-datasources enabled', () => { ); cy.contains('h1', 'Get started'); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); + cy.get('[data-test-subj="dataSourceSelectableButton"]').should('contain', '9202'); cy.get('[data-test-subj="purge-cache"]').click(); cy.get('[class="euiToast euiToast--success euiGlobalToastListItem"]') @@ -233,10 +230,7 @@ describe('Multi-datasources enabled', () => { `http://localhost:5601/app/security-dashboards-plugin${externalDataSourceUrl}#/roles` ); - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( - 'contain', - '9202' - ); + cy.get('[data-test-subj="dataSourceSelectableButton"]').should('contain', '9202'); cy.get('[data-test-subj="tableHeaderCell_roleName_0"]').click(); cy.get('[data-test-subj="checkboxSelectRow-9202-role"]').should('exist'); From 9d60f4f23d6ecbb5e138e58714fc15398ba45b72 Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 26 Apr 2024 11:41:59 -0400 Subject: [PATCH 21/22] Remove datasource testing matrix Signed-off-by: Derek Ho --- .github/workflows/integration-test.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 3324f5376..2126ff1d0 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,7 +6,7 @@ env: TEST_BROWSER_HEADLESS: 1 CI: 1 PLUGIN_NAME: opensearch-security - OPENSEARCH_INITIAL_ADMIN_PASSWORD: admin + OPENSEARCH_INITIAL_ADMIN_PASSWORD: myStrongPassword123! jobs: tests: @@ -15,7 +15,6 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest , windows-latest ] - datasource_version: [2.4.0, 2.13.0, 3.0.0] runs-on: ${{ matrix.os }} steps: @@ -38,10 +37,10 @@ jobs: - name: Download security plugin and create setup scripts for remote cluster uses: ./.github/actions/download-plugin with: - opensearch-version: ${{ matrix.datasource_version }} + opensearch-version: ${{ env.OPENSEARCH_VERSION }} plugin-name: ${{ env.PLUGIN_NAME }} - download-location: ${{env.PLUGIN_NAME}}-${{matrix.datasource_version}} - plugin-version: ${{matrix.datasource_version}}.0 + download-location: ${{env.PLUGIN_NAME}}-${{ env.OPENSEARCH_VERSION }} + plugin-version: ${{ env.PLUGIN_VERSION }} - name: Download security plugin and create setup scripts for local cluster uses: ./.github/actions/download-plugin @@ -54,8 +53,8 @@ jobs: - name: Run Opensearch with A Single Plugin Remote Cluster uses: derek-ho/start-opensearch@v4 with: - opensearch-version: ${{ matrix.datasource_version }} - plugins: "file:$(pwd)/opensearch-security-${{matrix.datasource_version}}.zip" + opensearch-version: ${{ env.OPENSEARCH_VERSION }} + plugins: "file:$(pwd)/opensearch-security-${{ env.OPENSEARCH_VERSION }}.zip" security-enabled: true admin-password: ${{ env.OPENSEARCH_INITIAL_ADMIN_PASSWORD }} security_config_file: ${{ inputs.security_config_file }} From ec0d2694439a424d525bafeb79aa6405378ebc3d Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Fri, 26 Apr 2024 12:01:01 -0400 Subject: [PATCH 22/22] Fix password Signed-off-by: Derek Ho --- test/jest_integration/security_entity_api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index 6e03da978..ecae09cac 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -489,7 +489,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { type: 'username_password', credentials: { username: 'admin', - password: 'admin', + password: 'myStrongPassword123!', }, }, },