From 0b7f56d5c24c85e0896a9721c07088f29e583823 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 13 Nov 2024 18:06:53 +0800 Subject: [PATCH 1/4] feat: add functional test for workspace assets Signed-off-by: SuZhou-Joe --- .../mds_workspace_assets.spec.js | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js new file mode 100644 index 000000000..e7f2a24d3 --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js @@ -0,0 +1,172 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../../../../utils/base_constants'; + +let sourceWorkspaceName = 'test_source_workspace'; +let targetWorkspaceName = 'test_target_workspace'; + +let sourceWorkspaceId = ''; +let targetWorkspaceId = ''; +let datasourceId = ''; + +const setupWorkspace = (workspaceName, datasourceId) => { + return cy + .createWorkspace({ + name: workspaceName, + settings: { + ...(datasourceId ? { dataSources: [datasourceId] } : {}), + }, + }) + .then((value) => { + // load sample data + cy.loadSampleDataForWorkspace('ecommerce', value, datasourceId); + cy.wrap(value); + }); +}; + +if (Cypress.env('WORKSPACE_ENABLED')) { + describe('Workspace assets', () => { + before(() => { + cy.deleteWorkspaceByName(sourceWorkspaceName); + cy.deleteWorkspaceByName(targetWorkspaceName); + if (Cypress.env('DATASOURCE_MANAGEMENT_ENABLED')) { + cy.createDataSourceNoAuth().then((result) => { + datasourceId = result[0]; + expect(datasourceId).to.be.a('string').that.is.not.empty; + setupWorkspace(sourceWorkspaceName, datasourceId).then( + (value) => (sourceWorkspaceId = value) + ); + setupWorkspace(targetWorkspaceName, datasourceId).then( + (value) => (targetWorkspaceId = value) + ); + }); + } else { + setupWorkspace(sourceWorkspaceName).then( + (value) => (sourceWorkspaceId = value) + ); + setupWorkspace(targetWorkspaceName).then( + (value) => (targetWorkspaceId = value) + ); + } + }); + + after(() => { + cy.deleteWorkspaceByName(sourceWorkspaceName); + cy.deleteWorkspaceByName(targetWorkspaceName); + sourceWorkspaceId = ''; + targetWorkspaceId = ''; + }); + + it('Should not list assets from other workspace and generate correct url for inspect url inside a workspace', () => { + cy.visit(`${BASE_PATH}/w/${sourceWorkspaceId}/app/objects`); + cy.getElementByTestId('savedObjectsTableRowTitle').should('exist'); + cy.getElementByTestId('savedObjectsTableColumn-workspace_column').should( + 'not.exist' + ); + cy.contains('opensearch_dashboards_sample_data_ecommerce'); + // Electron old version may not support search event, so we manually trigger a search event + cy.getElementByTestId('savedObjectSearchBar') + .type('opensearch_dashboards_sample_data_ecommerce{enter}') + .trigger('search'); + cy.getElementByTestId('savedObjectsTableRowTitle').should( + 'have.length', + 1 + ); + + // Click more -> inspect + cy.getElementByTestId('euiCollapsedItemActionsButton') + .click() + .getElementByTestId('savedObjectsTableAction-inspect') + .click(); + cy.location('pathname').should( + 'include', + `/w/${sourceWorkspaceId}/app/indexPatterns` + ); + }); + + it('Should list assets from workspaces with permission and generate correct url for inspect url outside workspace', () => { + cy.visit(`${BASE_PATH}/app/objects`); + cy.getElementByTestId('savedObjectsTableColumn-workspace_column').should( + 'exist' + ); + // Electron old version may not support search event, so we manually trigger a search event + cy.getElementByTestId('savedObjectSearchBar') + .type('opensearch_dashboards_sample_data_ecommerce{enter}') + .trigger('search'); + cy.getElementByTestId('savedObjectsTableRowTitle').should( + 'have.length', + 2 + ); + + // Find the row belong to the target workspace + cy.contains(targetWorkspaceName) + .parents('.euiTableRow') + // Click more -> inspect + .find('[data-test-subj="euiCollapsedItemActionsButton"]') + .click() + .getElementByTestId('savedObjectsTableAction-inspect') + .click(); + cy.location('pathname').should( + 'include', + `/w/${targetWorkspaceId}/app/indexPatterns` + ); + }); + + it('Should not show set as default button when outside workspace', () => { + cy.request({ + url: `${BASE_PATH}/api/opensearch-dashboards/management/saved_objects/_find?workspaces=${targetWorkspaceId}&page=1&type=index-pattern`, + headers: { + 'Osd-Xsrf': 'osd-fetch', + }, + }) + .then((resp) => { + cy.wrap(resp.body.saved_objects).should('have.length', 1); + cy.wrap(resp.body.saved_objects[0].id); + }) + .then((indexPatternId) => { + cy.visit(`${BASE_PATH}/app/indexPatterns/patterns/${indexPatternId}`); + cy.contains('opensearch_dashboards_sample_data_ecommerce'); + cy.getElementByTestId('setDefaultIndexPatternButton').should( + 'not.exist' + ); + }); + }); + + it('Short url should be able to be generated multiple times and preserve workspace info', () => { + cy.visit(`${BASE_PATH}/w/${sourceWorkspaceId}/app/visualize`); + cy.getElementByTestId('visListingTitleLink-[eCommerce]-Markdown').click(); + cy.getElementByTestId('shareTopNavButton') + .click() + .getElementByTestId('sharePanel-Permalinks') + .click() + .getElementByTestId('useShortUrl') + .as('generateShortUrlButton') + .click(); + + // Should generate successfully + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .should('include', `${BASE_PATH}/goto`); + + // Regeneration should work without error + cy.get('@generateShortUrlButton').click(); + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .should('include', `${BASE_PATH}/w/${sourceWorkspaceId}`); + cy.get('@generateShortUrlButton').click(); + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .then((shortUrl) => { + cy.wrap(shortUrl).should('include', `${BASE_PATH}/goto`); + cy.visit(`${BASE_PATH}/w/${sourceWorkspaceId}/app/objects`); + cy.contains('Workspace assets'); + cy.visit(shortUrl); + cy.location('pathname').should('include', `/w/${sourceWorkspaceId}`); + cy.contains('[eCommerce] Markdown'); + }); + }); + }); +} From 07976c48fd8030c7d7ed2439a49c053ae7992acd Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 14 Nov 2024 10:33:46 +0800 Subject: [PATCH 2/4] feat: make it compatible with mds case Signed-off-by: SuZhou-Joe --- .../workspace-plugin/mds_workspace_assets.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js index e7f2a24d3..17d87a15c 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_assets.spec.js @@ -137,7 +137,7 @@ if (Cypress.env('WORKSPACE_ENABLED')) { it('Short url should be able to be generated multiple times and preserve workspace info', () => { cy.visit(`${BASE_PATH}/w/${sourceWorkspaceId}/app/visualize`); - cy.getElementByTestId('visListingTitleLink-[eCommerce]-Markdown').click(); + cy.contains(/\[eCommerce\] Markdown/).click(); cy.getElementByTestId('shareTopNavButton') .click() .getElementByTestId('sharePanel-Permalinks') @@ -165,7 +165,7 @@ if (Cypress.env('WORKSPACE_ENABLED')) { cy.contains('Workspace assets'); cy.visit(shortUrl); cy.location('pathname').should('include', `/w/${sourceWorkspaceId}`); - cy.contains('[eCommerce] Markdown'); + cy.contains(/\[eCommerce\] Markdown/); }); }); }); From 1ea479ed1da10789c27e75ffbc678fe55e783f20 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 15 Nov 2024 10:46:03 +0800 Subject: [PATCH 3/4] feat: add acl related test cases Signed-off-by: SuZhou-Joe --- .../mds_workspace_acl.spec.js | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js new file mode 100644 index 000000000..4cc76c4ec --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js @@ -0,0 +1,230 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../../../../utils/base_constants'; +import { ADMIN_AUTH } from '../../../../utils/commands'; +import workspaceTestUser from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestUser.json'; +import workspaceTestRole from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestRole.json'; +import workspaceTestRoleMapping from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestRoleMapping.json'; +import { WORKSPACE_API_PREFIX } from '../../../../utils/dashboards/workspace-plugin/constants'; + +let noPermissionWorkspaceName = 'acl_no_permission_workspace'; +let readOnlyWorkspaceName = 'acl_readonly_workspace'; +let libraryWriteWorkspaceName = 'acl_library_write_workspace'; +let ownerWorkspaceName = 'acl_owner_workspace'; + +let noPermissionWorkspaceId = ''; +let readOnlyWorkspaceId = ''; +let libraryWriteWorkspaceId = ''; +let ownerWorkspaceId = ''; +let datasourceId = ''; + +const getPolicy = (permission, userName) => ({ + [permission]: { + users: [userName], + }, +}); + +const NONE_DASHBOARDS_ADMIN_USERNAME = 'workspace-acl-test'; +const WORKSPACE_TEST_ROLE_NAME = 'workspace-acl-test-role'; + +const ACLPolicyMap = { + [noPermissionWorkspaceName]: {}, + [readOnlyWorkspaceName]: { + ...getPolicy('read', NONE_DASHBOARDS_ADMIN_USERNAME), + ...getPolicy('library_read', NONE_DASHBOARDS_ADMIN_USERNAME), + }, + [libraryWriteWorkspaceName]: { + ...getPolicy('read', NONE_DASHBOARDS_ADMIN_USERNAME), + ...getPolicy('library_write', NONE_DASHBOARDS_ADMIN_USERNAME), + }, + [ownerWorkspaceName]: { + ...getPolicy('write', NONE_DASHBOARDS_ADMIN_USERNAME), + ...getPolicy('library_write', NONE_DASHBOARDS_ADMIN_USERNAME), + }, +}; + +const setupWorkspace = (workspaceName, datasourceId) => { + return cy + .createWorkspace({ + name: workspaceName, + settings: { + ...(datasourceId ? { dataSources: [datasourceId] } : {}), + permissions: ACLPolicyMap[workspaceName], + }, + }) + .then((value) => { + // load sample data + cy.loadSampleDataForWorkspace('ecommerce', value, datasourceId); + cy.wrap(value); + }); +}; + +if ( + Cypress.env('WORKSPACE_ENABLED') && + Cypress.env('SAVED_OBJECTS_PERMISSION_ENABLED') && + Cypress.env('SECURITY_ENABLED') +) { + describe('Workspace ACL', () => { + const originalUser = ADMIN_AUTH.username; + const originalPassword = ADMIN_AUTH.password; + before(() => { + cy.deleteWorkspaceByName(noPermissionWorkspaceName); + cy.deleteWorkspaceByName(readOnlyWorkspaceName); + cy.deleteWorkspaceByName(libraryWriteWorkspaceName); + cy.deleteWorkspaceByName(ownerWorkspaceName); + + cy.createInternalUser(NONE_DASHBOARDS_ADMIN_USERNAME, workspaceTestUser); + cy.createRole(WORKSPACE_TEST_ROLE_NAME, workspaceTestRole); + cy.createRoleMapping(WORKSPACE_TEST_ROLE_NAME, workspaceTestRoleMapping); + + if (Cypress.env('DATASOURCE_MANAGEMENT_ENABLED')) { + cy.createDataSourceNoAuth().then((result) => { + datasourceId = result[0]; + expect(datasourceId).to.be.a('string').that.is.not.empty; + setupWorkspace(noPermissionWorkspaceName, datasourceId).then( + (value) => (noPermissionWorkspaceId = value) + ); + setupWorkspace(readOnlyWorkspaceName, datasourceId).then( + (value) => (readOnlyWorkspaceId = value) + ); + setupWorkspace(libraryWriteWorkspaceName, datasourceId).then( + (value) => (libraryWriteWorkspaceId = value) + ); + setupWorkspace(ownerWorkspaceName, datasourceId).then( + (value) => (ownerWorkspaceId = value) + ); + }); + } else { + setupWorkspace(noPermissionWorkspaceName, datasourceId).then( + (value) => (noPermissionWorkspaceId = value) + ); + setupWorkspace(readOnlyWorkspaceName, datasourceId).then( + (value) => (readOnlyWorkspaceId = value) + ); + setupWorkspace(libraryWriteWorkspaceName, datasourceId).then( + (value) => (libraryWriteWorkspaceId = value) + ); + setupWorkspace(ownerWorkspaceName, datasourceId).then( + (value) => (ownerWorkspaceId = value) + ); + } + }); + + after(() => { + cy.deleteWorkspaceByName(noPermissionWorkspaceName); + cy.deleteWorkspaceByName(readOnlyWorkspaceName); + cy.deleteWorkspaceByName(libraryWriteWorkspaceName); + cy.deleteWorkspaceByName(ownerWorkspaceName); + readOnlyWorkspaceId = ''; + libraryWriteWorkspaceId = ''; + + ADMIN_AUTH.newUser = originalUser; + ADMIN_AUTH.newPassword = originalPassword; + cy.deleteRoleMapping(WORKSPACE_TEST_ROLE_NAME); + cy.deleteInternalUser(NONE_DASHBOARDS_ADMIN_USERNAME); + cy.deleteRole(WORKSPACE_TEST_ROLE_NAME); + }); + + describe('Normal user', () => { + beforeEach(() => { + ADMIN_AUTH.newUser = NONE_DASHBOARDS_ADMIN_USERNAME; + ADMIN_AUTH.newPassword = workspaceTestUser.password; + }); + + it('Normal user should not be able to create workspace', () => { + cy.request({ + method: 'POST', + url: `${BASE_PATH}${WORKSPACE_API_PREFIX}`, + headers: { + 'osd-xsrf': true, + }, + body: { + attributes: { + name: 'test_workspace', + features: ['use-case-observability'], + description: 'test_description', + }, + }, + failOnStatusCode: false, + }).then((resp) => + cy + .wrap(resp.body.error) + .should('equal', 'Invalid permission, please contact OSD admin') + ); + }); + + it('Normal users should only see the workspaces they have permission with', () => { + cy.visit(`${BASE_PATH}/app/home`); + cy.contains(readOnlyWorkspaceName); + cy.contains(noPermissionWorkspaceName).should('not.exist'); + }); + + it('Readonly users should not be allowed to update dashboards/visualizations within the workspace', () => { + cy.visit(`${BASE_PATH}/w/${readOnlyWorkspaceId}/app/visualize`); + cy.contains(/\[eCommerce\] Markdown/).click(); + cy.getElementByTestId('visualizeSaveButton').click(); + cy.getElementByTestId('confirmSaveSavedObjectButton').click(); + cy.contains('Forbidden'); + }); + + it('Normal users should not be allowed to visit workspace he/she has no permission', () => { + cy.visit(`${BASE_PATH}/w/${noPermissionWorkspaceId}/app/objects`); + cy.contains('Invalid saved objects permission'); + }); + + it('Normal users should only see the workspaces he has library_write permission in the target workspaces list of duplicate modal', () => { + cy.visit(`${BASE_PATH}/w/${readOnlyWorkspaceId}/app/objects`); + cy.getElementByTestId('savedObjectsTableRowTitle').should('exist'); + cy.getElementByTestId('duplicateObjects') + .click() + .getElementByTestId('savedObjectsDuplicateModal') + .find('[data-test-subj="comboBoxInput"]') + .click(); + + cy.contains(libraryWriteWorkspaceName); + cy.contains(ownerWorkspaceName); + cy.contains(noPermissionWorkspaceName).should('not.exist'); + }); + + it('Users should not be able to update default index pattern / default data source if he/she is not the workspace owner', () => { + cy.visit(`${BASE_PATH}/w/${libraryWriteWorkspaceId}/app/indexPatterns`); + cy.contains('opensearch_dashboards_sample_data_ecommerce').click(); + cy.getElementByTestId('setDefaultIndexPatternButton').click(); + cy.contains('Unable to update UI setting'); + }); + + it('Normal users should not be able to find objects from other workspaces when inside a workspace', () => { + cy.visit(`${BASE_PATH}/w/${readOnlyWorkspaceId}/app/objects`); + cy.getElementByTestId('savedObjectsTableRowTitle').should('exist'); + cy.getElementByTestId( + 'savedObjectsTableColumn-workspace_column' + ).should('not.exist'); + cy.contains('opensearch_dashboards_sample_data_ecommerce'); + // Electron old version may not support search event, so we manually trigger a search event + cy.getElementByTestId('savedObjectSearchBar') + .type('opensearch_dashboards_sample_data_ecommerce{enter}') + .trigger('search'); + cy.getElementByTestId('savedObjectsTableRowTitle').should( + 'have.length', + 1 + ); + }); + + if (Cypress.env('DATASOURCE_MANAGEMENT_ENABLED')) { + it('Normal users should not be able to associate / dissociate data sources from workspace.', () => { + cy.visit(`${BASE_PATH}/w/${ownerWorkspaceId}/app/dataSources`); + cy.contains('Data sources'); + cy.getElementByTestId('workspaceAssociateDataSourceButton').should( + 'not.exist' + ); + cy.getElementByTestId( + 'dataSourcesManagement-dataSourceTable-dissociateButton' + ).should('not.exist'); + }); + } + }); + }); +} From 6ad7a5498ea094f411d2121fc4bea4ffcd93ca5f Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 15 Nov 2024 12:28:58 +0800 Subject: [PATCH 4/4] feat: update Signed-off-by: SuZhou-Joe --- .../workspace-plugin/mds_workspace_acl.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js index 4cc76c4ec..2a07fe083 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/workspace-plugin/mds_workspace_acl.spec.js @@ -7,7 +7,6 @@ import { BASE_PATH } from '../../../../utils/base_constants'; import { ADMIN_AUTH } from '../../../../utils/commands'; import workspaceTestUser from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestUser.json'; import workspaceTestRole from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestRole.json'; -import workspaceTestRoleMapping from '../../../../fixtures/dashboard/opensearch_dashboards/workspace/workspaceTestRoleMapping.json'; import { WORKSPACE_API_PREFIX } from '../../../../utils/dashboards/workspace-plugin/constants'; let noPermissionWorkspaceName = 'acl_no_permission_workspace'; @@ -78,7 +77,9 @@ if ( cy.createInternalUser(NONE_DASHBOARDS_ADMIN_USERNAME, workspaceTestUser); cy.createRole(WORKSPACE_TEST_ROLE_NAME, workspaceTestRole); - cy.createRoleMapping(WORKSPACE_TEST_ROLE_NAME, workspaceTestRoleMapping); + cy.createRoleMapping(WORKSPACE_TEST_ROLE_NAME, { + users: [NONE_DASHBOARDS_ADMIN_USERNAME], + }); if (Cypress.env('DATASOURCE_MANAGEMENT_ENABLED')) { cy.createDataSourceNoAuth().then((result) => {