diff --git a/cypress/integration/plugins/security-dashboards-plugin/change_tenant_successfully.js b/cypress/integration/plugins/security-dashboards-plugin/change_tenant_successfully.js new file mode 100644 index 000000000..76e44ae2a --- /dev/null +++ b/cypress/integration/plugins/security-dashboards-plugin/change_tenant_successfully.js @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CURRENT_TENANT } from '../../../utils/commands'; +import { switchTenantTo } from './switch_tenant'; + +if (Cypress.env('SECURITY_ENABLED')) { + describe('Switch tenants when visiting copied links: ', () => { + const tenantName = 'private'; + + before(() => { + cy.server(); + }); + it('Checks that the tenant switcher can switch tenants despite a different tenant being present in the tenant query parameter.', function () { + CURRENT_TENANT.newTenant = tenantName; + + cy.visit('/app/home').then(() => { + cy.waitForLoader(); + switchTenantTo('global'); + cy.waitForLoader(); + cy.getElementByTestId('account-popover').click(); + cy.get('#tenantName').should('contain.text', 'Global'); + }); + }); + }); +} diff --git a/cypress/integration/plugins/security-dashboards-plugin/switch_tenant.js b/cypress/integration/plugins/security-dashboards-plugin/switch_tenant.js new file mode 100644 index 000000000..5666eca8f --- /dev/null +++ b/cypress/integration/plugins/security-dashboards-plugin/switch_tenant.js @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function switchTenantTo(newTenant) { + cy.getElementByTestId('account-popover').click(); + cy.intercept({ + method: 'GET', + url: '/api/v1/auth/dashboardsinfo', + }).as('waitForDashboardsInfo'); + + cy.intercept({ + method: 'GET', + url: '/api/v1/configuration/account', + }).as('waitForAccountInfo'); + + cy.getElementByTestId('switch-tenants').click(); + //should ensures the dialog window is fully loaded and the radios can be selected. + cy.get('[id="' + newTenant + '"][name="tenantSwitchRadios"]').should( + 'be.enabled' + ); + cy.get('.euiRadio__label[for="' + newTenant + '"]').click(); + + cy.intercept({ + method: 'POST', + url: '/api/v1/multitenancy/tenant', + }).as('waitForUpdatingTenants'); + cy.getElementByTestId('tenant-switch-modal') + .find('[data-test-subj="confirm"]') + .click(); + + cy.wait('@waitForUpdatingTenants'); + + // Make sure dashboards has really reloaded. + // @waitForReloadAfterTenantSwitch should be triggered twice + cy.wait('@waitForDashboardsInfo'); + cy.wait('@waitForDashboardsInfo'); +} diff --git a/cypress/integration/plugins/security-dashboards-plugin/tenancy_change_on_shortlink.js b/cypress/integration/plugins/security-dashboards-plugin/tenancy_change_on_shortlink.js new file mode 100644 index 000000000..eddc3cb94 --- /dev/null +++ b/cypress/integration/plugins/security-dashboards-plugin/tenancy_change_on_shortlink.js @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CURRENT_TENANT } from '../../../utils/commands'; +import { switchTenantTo } from './switch_tenant'; +import indexPatternGlobalTenantHeaderSetUp from '../../../fixtures/plugins/security-dashboards-plugin/indexpatterns/indexPatternGlobalTenantHeader.json'; +import indexPatternPrivateTenantHeaderSetUp from '../../../fixtures/plugins/security-dashboards-plugin/indexpatterns/indexPatternPrivateTenantHeader.json'; + +if (Cypress.env('SECURITY_ENABLED')) { + describe('Multi Tenancy Tests: ', () => { + before(() => { + cy.server(); + + cy.createIndexPattern( + 'index-pattern1', + { + title: 's*', + timeFieldName: 'timestamp', + }, + indexPatternGlobalTenantHeaderSetUp + ); + + cy.createIndexPattern( + 'index-pattern2', + { + title: 'se*', + timeFieldName: 'timestamp', + }, + indexPatternPrivateTenantHeaderSetUp + ); + }); + + it('Tests that when the short URL is copied and pasted, it will route correctly with the right tenant', function () { + const randomNumber = Cypress._.random(0, 1e6); + const dashboardName = 'Cypress dashboard - ' + randomNumber; + // We are programmatically creating a dashboard so that the test + // always have the same view. An empty list would show the empty prompt. + // Also, this saves us some typing, clicking and waiting in the test. + cy.createDashboard( + { + title: dashboardName, + }, + { + security_tenant: 'private', + } + ); + + // When creating the shortUrl, we don't want to have the security_tenant + // parameter in the url - otherwise it will be stored in the shortUrl + // itself, which would make this test obsolete. + // But it is also hard to get the tests running reliably when opening + // Dashboards without the parameter (tenant selector popup etc.). + // Hence, we do some navigation to "lose" the query parameter. + CURRENT_TENANT.newTenant = 'private'; + cy.visit('/app/home', { + waitForGetTenant: true, + onBeforeLoad(window) { + // set up session storage as we would expect to emulate browser + window.sessionStorage.setItem( + 'opendistro::security::tenant::show_popup', + false + ); + + window.localStorage.setItem( + 'opendistro::security::tenant::saved', + '__user__' + ); + }, + }); + // Navigate to the Dashboards app + cy.getElementByTestId('toggleNavButton').should('be.visible').click(); + // After clicking the navigation, the security_tenant parameter should be gone + cy.get('[href$="/app/dashboards#/list"]').should('be.visible').click(); + + // The test subj seems to replace spaces with a dash, so we convert the dashboard name here too. + // Go to the dashboard we have created + const selectorDashboardName = dashboardName.split(' ').join('-'); + cy.getElementByTestId( + 'dashboardListingTitleLink-' + selectorDashboardName + ) + .should('be.visible') + .click(); + + cy.getElementByTestId('savedObjectTitle').type(dashboardName); + + cy.intercept({ + method: 'POST', + url: '/api/saved_objects/_bulk_get', + }).as('waitForReloadingDashboard'); + cy.getElementByTestId('confirmSaveSavedObjectButton').click(); + cy.wait('@waitForReloadingDashboard'); + cy.wait(2000); + + // 2. Open top share navigation to access copy short url + cy.getElementByTestId('shareTopNavButton').click(); + cy.getElementByTestId('sharePanel-Permalinks').click(); + + // 3. Create the short url, wait for response + cy.intercept('POST', '/api/shorten_url').as('getShortUrl'); + // If the url already contains the tenant parameter, it will be stored in the short url. That will work in the app + // but would render this test useless. We're testing that resolved short urls without the tenant parameter work as well. + cy.url().should('not.contain', 'security_tenant'); + cy.getElementByTestId('createShortUrl').click(); + cy.wait('@getShortUrl'); + + //4. Switch tenant & visit shortURL link to ensure tenant from short URL is retained + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .should('contain', '/goto/') + .then((shortUrl) => { + cy.log('Short url is ' + shortUrl); + // Navigate away to avoid the non existing dashboard in the next tenant. + switchTenantTo('global'); + + // Since we can't reliably read the clipboard data, we have to append the tenant parameter manually + cy.visit(shortUrl + '?security_tenant=private', { + excludeTenant: true, // We are passing the tenant as a query parameter. Mainly because of readability. + onBeforeLoad(window) { + // Here we are simulating the new tab scenario which isn't supported by Cypress + window.sessionStorage.clear(); + }, + }); + + cy.url({ timeout: 10000 }).should('contain', 'security_tenant='); + cy.getElementByTestId('breadcrumb last').should( + 'contain.text', + dashboardName + ); + }); + }); + after(() => { + cy.deleteIndexPattern('index-pattern1', { + headers: { + securitytenant: ['global'], + 'osd-xsrf': true, + }, + }); + cy.deleteIndexPattern('index-pattern2', { + headers: { + securitytenant: ['private'], + 'osd-xsrf': true, + }, + }); + }); + }); +} diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index cab746dd3..2c9076263 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -46,7 +46,13 @@ Cypress.Commands.overwrite('visit', (orig, url, options) => { auth: ADMIN_AUTH, }; } - newOptions.qs = { security_tenant: CURRENT_TENANT.defaultTenant }; + if (!newOptions.excludeTenant) { + newOptions.qs = { + ...newOptions.qs, + security_tenant: CURRENT_TENANT.defaultTenant, + }; + } + if (waitForGetTenant) { cy.intercept('GET', '/api/v1/multitenancy/tenant').as('getTenant'); orig(url, newOptions); @@ -367,6 +373,23 @@ Cypress.Commands.add('createIndexPattern', (id, attributes, header = {}) => { }); }); +Cypress.Commands.add('createDashboard', (attributes = {}, headers = {}) => { + const url = `${Cypress.config().baseUrl}/api/saved_objects/dashboard`; + + cy.request({ + method: 'POST', + url, + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + ...headers, + }, + body: JSON.stringify({ + attributes, + }), + }); +}); + Cypress.Commands.add('changeDefaultTenant', (attributes, header = {}) => { const url = Cypress.env('openSearchUrl') + '/_plugins/_security/api/tenancy/config'; diff --git a/cypress/utils/index.d.ts b/cypress/utils/index.d.ts index d5daf9dfd..8b65b8390 100644 --- a/cypress/utils/index.d.ts +++ b/cypress/utils/index.d.ts @@ -105,6 +105,20 @@ declare namespace Cypress { header: string, ): Chainable; + /** + * Adds a dashboard + * @example + * cy.createDashboard({ title: 'My dashboard'}) + */ + createDashboard( + attributes: { + title: string; + [key: string]: any; + }, + headers?: { + [key: string]: any; + } + ): Chainable; /** * Changes the Default tenant for the domain.