diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 59f2e1895..cc970b85a 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -42,7 +42,7 @@ jobs: WORKDIR /opensearch/ ENTRYPOINT /docker-host/os-ep.sh EOF - docker run -d -p 9200:9200 -p 9600:9600 -i opensearch-test:latest + docker run -d --network=host -i opensearch-test:latest - name: Checkout OpenSearch Dashboard uses: actions/checkout@v2 @@ -103,6 +103,7 @@ jobs: run: | cd ./OpenSearch-Dashboards yarn osd bootstrap + node scripts/build_opensearch_dashboards_platform_plugins.js - name: Run integration tests run: | diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index e78086018..d03833975 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -55,6 +55,7 @@ plugins.security.allow_default_init_securityindex: true plugins.security.authcz.admin_dn: - CN=kirk,OU=client,O=client,L=test, C=de +plugins.security.unsupported.restapi.allow_securityconfig_modification: true plugins.security.audit.type: internal_opensearch plugins.security.enable_snapshot_restore_privilege: true plugins.security.check_snapshot_restore_write_privileges: true @@ -117,6 +118,8 @@ Next, go to the base directory and run `yarn osd bootstrap` to install any addit Now, from the base directory and run `yarn start`. This should start dashboard UI successfully. `Cmd+click` the url in the console output (It should look something like `http://0:5601/omf`). Once the page loads, you should be able to log in with user `admin` and password `admin`. +To run selenium based integration tests, download and export the firefox web-driver to your PATH. Also, run `node scripts/build_opensearch_dashboards_platform_plugins.js` or `yarn start` before running the tests. This is essential to generate the bundles. + ## Submitting Changes See [CONTRIBUTING](CONTRIBUTING.md). diff --git a/package.json b/package.json index 5074cf504..c811fb0e8 100644 --- a/package.json +++ b/package.json @@ -17,19 +17,23 @@ "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" }, "devDependencies": { "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards", - "typescript": "4.0.2", - "gulp-rename": "2.0.0", "@testing-library/react-hooks": "^7.0.2", - "@types/hapi__wreck": "^15.0.1" + "@types/hapi__wreck": "^15.0.1", + "gulp-rename": "2.0.0", + "saml-idp": "^1.2.1", + "selenium-webdriver": "^4.0.0-alpha.7", + "selfsigned": "^2.0.1", + "typescript": "4.0.2" }, "dependencies": { - "@hapi/wreck": "^17.1.0", "@hapi/cryptiles": "5.0.0", + "@hapi/wreck": "^17.1.0", "html-entities": "1.3.1" } } diff --git a/public/apps/account/account-nav-button.tsx b/public/apps/account/account-nav-button.tsx index 1100e9f31..7bd0e578b 100644 --- a/public/apps/account/account-nav-button.tsx +++ b/public/apps/account/account-nav-button.tsx @@ -93,7 +93,11 @@ export function AccountNavButton(props: { {resolveTenantName(props.tenant || '', username)}} + label={ + + {resolveTenantName(props.tenant || '', username)} + + } /> @@ -140,7 +144,7 @@ export function AccountNavButton(props: { ); return ( - + {props.divider} ); + } else if (props.authType === 'saml') { + return ( +
+ {props.divider} + samlLogout(props.http)} + > + Log out + +
+ ); } else if (props.authType === 'proxy') { return
; } else { @@ -45,7 +59,7 @@ export function LogoutButton(props: {
{props.divider} logout(props.http, props.logoutUrl)} diff --git a/public/apps/account/test/__snapshots__/account-nav-button.test.tsx.snap b/public/apps/account/test/__snapshots__/account-nav-button.test.tsx.snap index c45a38eee..39b9e332f 100644 --- a/public/apps/account/test/__snapshots__/account-nav-button.test.tsx.snap +++ b/public/apps/account/test/__snapshots__/account-nav-button.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Account navigation button renders 1`] = ` - + tenant1 diff --git a/public/apps/account/test/__snapshots__/log-out-button.test.tsx.snap b/public/apps/account/test/__snapshots__/log-out-button.test.tsx.snap index abdbbdc84..c3c476b25 100644 --- a/public/apps/account/test/__snapshots__/log-out-button.test.tsx.snap +++ b/public/apps/account/test/__snapshots__/log-out-button.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Account menu - Log out button renders renders when auth type is OpenId
@@ -20,7 +20,7 @@ exports[`Account menu - Log out button renders renders when auth type is SAML 1` Log out @@ -32,7 +32,7 @@ exports[`Account menu - Log out button renders renders when auth type is not Ope
diff --git a/public/apps/account/test/log-out-button.test.tsx b/public/apps/account/test/log-out-button.test.tsx index 6021cbad9..e8874c5ba 100644 --- a/public/apps/account/test/log-out-button.test.tsx +++ b/public/apps/account/test/log-out-button.test.tsx @@ -68,7 +68,7 @@ describe('Account menu - Log out button', () => { const component = shallow( ); - component.find('[data-test-subj="log-out-2"]').simulate('click'); + component.find('[data-test-subj="log-out-3"]').simulate('click'); expect(logout).toBeCalled(); }); diff --git a/public/apps/account/utils.tsx b/public/apps/account/utils.tsx index 2a871066c..637f700fa 100644 --- a/public/apps/account/utils.tsx +++ b/public/apps/account/utils.tsx @@ -40,6 +40,12 @@ 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}${API_AUTH_LOGOUT}`; +} + export async function updateNewPassword( http: HttpStart, newPassword: string, diff --git a/server/auth/types/saml/routes.ts b/server/auth/types/saml/routes.ts index 808dfa8ae..79454272c 100644 --- a/server/auth/types/saml/routes.ts +++ b/server/auth/types/saml/routes.ts @@ -46,6 +46,7 @@ export class SamlAuthRoutes { validate: validateNextUrl, }) ), + redirectHash: schema.string(), }), }, options: { @@ -67,6 +68,7 @@ export class SamlAuthRoutes { saml: { nextUrl: request.query.nextUrl, requestId: samlHeader.requestId, + redirectHash: request.query.redirectHash === 'true', }, }; this.sessionStorageFactory.asScoped(request).set(cookie); @@ -95,6 +97,7 @@ export class SamlAuthRoutes { async (context, request, response) => { let requestId: string = ''; let nextUrl: string = '/'; + let redirectHash: boolean = false; try { const cookie = await this.sessionStorageFactory.asScoped(request).get(); if (cookie) { @@ -102,6 +105,7 @@ export class SamlAuthRoutes { nextUrl = cookie.saml?.nextUrl || `${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`; + redirectHash = cookie.saml?.redirectHash || false; } if (!requestId) { return response.badRequest({ @@ -143,11 +147,21 @@ export class SamlAuthRoutes { expiryTime, }; this.sessionStorageFactory.asScoped(request).set(cookie); - return response.redirected({ - headers: { - location: nextUrl, - }, - }); + if (redirectHash) { + return response.redirected({ + headers: { + location: `${ + this.coreSetup.http.basePath.serverBasePath + }/auth/saml/redirectUrlFragment?nextUrl=${escape(nextUrl)}`, + }, + }); + } else { + return response.redirected({ + headers: { + location: nextUrl, + }, + }); + } } catch (error) { context.security_plugin.logger.error( `SAML SP initiated authentication workflow failed: ${error}` @@ -215,6 +229,119 @@ export class SamlAuthRoutes { } ); + // captureUrlFragment is the first route that will be invoked in the SP initiated login. + // This route will execute the captureUrlFragment.js script. + this.coreSetup.http.resources.register( + { + path: '/auth/saml/captureUrlFragment', + validate: { + query: schema.object({ + nextUrl: schema.maybe( + schema.string({ + validate: validateNextUrl, + }) + ), + }), + }, + options: { + authRequired: false, + }, + }, + async (context, request, response) => { + this.sessionStorageFactory.asScoped(request).clear(); + const serverBasePath = this.coreSetup.http.basePath.serverBasePath; + return response.renderHtml({ + body: ` + + OSD SAML Capture + + + `, + }); + } + ); + + // This script will store the URL Hash in browser's local storage. + this.coreSetup.http.resources.register( + { + path: '/auth/saml/captureUrlFragment.js', + validate: false, + options: { + authRequired: false, + }, + }, + async (context, request, response) => { + this.sessionStorageFactory.asScoped(request).clear(); + return response.renderJs({ + body: `let samlHash=window.location.hash.toString(); + let redirectHash = false; + if (samlHash !== "") { + window.localStorage.removeItem('samlHash'); + window.localStorage.setItem('samlHash', samlHash); + redirectHash = true; + } + let params = new URLSearchParams(window.location.search); + let nextUrl = params.get("nextUrl"); + finalUrl = "login?nextUrl=" + encodeURIComponent(nextUrl); + finalUrl += "&redirectHash=" + encodeURIComponent(redirectHash); + window.location.replace(finalUrl); + + `, + }); + } + ); + + // Once the User is authenticated via the '_opendistro/_security/saml/acs' route, + // the browser will be redirected to '/auth/saml/redirectUrlFragment' route, + // which will execute the redirectUrlFragment.js. + this.coreSetup.http.resources.register( + { + path: '/auth/saml/redirectUrlFragment', + validate: { + query: schema.object({ + nextUrl: schema.any(), + }), + }, + options: { + authRequired: true, + }, + }, + async (context, request, response) => { + const serverBasePath = this.coreSetup.http.basePath.serverBasePath; + return response.renderHtml({ + body: ` + + OSD SAML Success + + + `, + }); + } + ); + + // This script will pop the Hash from local storage if it exists. + // And forward the browser to the next url. + this.coreSetup.http.resources.register( + { + path: '/auth/saml/redirectUrlFragment.js', + validate: false, + options: { + authRequired: true, + }, + }, + async (context, request, response) => { + return response.renderJs({ + body: `let samlHash=window.localStorage.getItem('samlHash'); + window.localStorage.removeItem('samlHash'); + let params = new URLSearchParams(window.location.search); + let nextUrl = params.get("nextUrl"); + finalUrl = nextUrl + samlHash; + window.location.replace(finalUrl); + `, + }); + } + ); + this.router.get( { path: `/auth/logout`, diff --git a/server/auth/types/saml/saml_auth.ts b/server/auth/types/saml/saml_auth.ts index d9e61718b..ee8762406 100644 --- a/server/auth/types/saml/saml_auth.ts +++ b/server/auth/types/saml/saml_auth.ts @@ -54,18 +54,19 @@ export class SamlAuthentication extends AuthenticationType { private generateNextUrl(request: OpenSearchDashboardsRequest): string { const path = this.coreSetup.http.basePath.serverBasePath + - (request.url.path || '/app/opensearch-dashboards'); + (request.url.pathname || '/app/opensearch-dashboards'); return escape(path); } - private redirectToLoginUri(request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) { + // Check if we can get the previous tenant information from the expired cookie. + private redirectSAMlCapture = (request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) => { const nextUrl = this.generateNextUrl(request); const clearOldVersionCookie = clearOldVersionCookieValue(this.config); return toolkit.redirected({ - location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/login?nextUrl=${nextUrl}`, + location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/captureUrlFragment?nextUrl=${nextUrl}`, 'set-cookie': clearOldVersionCookie, }); - } + }; private setupRoutes(): void { const samlAuthRoutes = new SamlAuthRoutes( @@ -97,6 +98,7 @@ export class SamlAuthentication extends AuthenticationType { }; } + // Can be improved to check if the token is expiring. async isValidCookie(cookie: SecuritySessionCookie): Promise { return ( cookie.authType === this.type && @@ -112,7 +114,7 @@ export class SamlAuthentication extends AuthenticationType { toolkit: AuthToolkit ): IOpenSearchDashboardsResponse | AuthResult { if (this.isPageRequest(request)) { - return this.redirectToLoginUri(request, toolkit); + return this.redirectSAMlCapture(request, toolkit); } else { return response.unauthorized(); } diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 597535f82..fb7c7d11a 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -157,6 +157,7 @@ export class SecurityClient { // location="https:///api/saml2/v1/sso?SAMLRequest=" // requestId="" // ' + if (!error.wwwAuthenticateDirective) { throw error; } diff --git a/server/session/security_cookie.ts b/server/session/security_cookie.ts index 7cd172a90..50b880d9b 100644 --- a/server/session/security_cookie.ts +++ b/server/session/security_cookie.ts @@ -36,6 +36,7 @@ export interface SecuritySessionCookie { saml?: { requestId?: string; nextUrl?: string; + redirectHash?: boolean; }; } diff --git a/test/helper/cookie.ts b/test/helper/cookie.ts index 381891f9a..dcbea1489 100644 --- a/test/helper/cookie.ts +++ b/test/helper/cookie.ts @@ -20,7 +20,7 @@ import { AUTHORIZATION_HEADER_NAME } from '../constant'; export function extractAuthCookie(response: Response) { const setCookieHeaders = response.header['set-cookie'] as string[]; - let securityAuthCookie: string; + let securityAuthCookie: string | null = null; for (const setCookie of setCookieHeaders) { if (setCookie.startsWith('security_authentication=')) { securityAuthCookie = setCookie.split(';')[0]; diff --git a/test/jest_integration/runIdpServer.js b/test/jest_integration/runIdpServer.js new file mode 100644 index 000000000..35533ae6c --- /dev/null +++ b/test/jest_integration/runIdpServer.js @@ -0,0 +1,32 @@ +/* + * 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 { runServer } = require('saml-idp'); + +const { generate } = require('selfsigned'); + +const pems = generate(null, { + keySize: 2048, + clientCertificateCN: '/C=US/ST=California/L=San Francisco/O=JankyCo/CN=Test Identity Provider', + days: 7300, +}); + +// Create certificate pair on the fly and pass it to runServer +runServer({ + acsUrl: 'http://localhost:5601/_opendistro/_security/saml/acs', + audience: 'https://localhost:9200', + cert: pems.cert, + key: pems.private.toString().replace(/\r\n/, '\n'), +}); diff --git a/test/jest_integration/saml_auth.test.ts b/test/jest_integration/saml_auth.test.ts new file mode 100644 index 000000000..53d2a951e --- /dev/null +++ b/test/jest_integration/saml_auth.test.ts @@ -0,0 +1,338 @@ +/* + * 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 osdTestServer from '../../../../src/core/test_helpers/osd_server'; +import { Root } from '../../../../src/core/server/root'; +import { resolve } from 'path'; +import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; +import { + ADMIN_CREDENTIALS, + OPENSEARCH_DASHBOARDS_SERVER_USER, + OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, +} from '../constant'; +import wreck from '@hapi/wreck'; +import { Builder, By, until } from 'selenium-webdriver'; +import { Options } from 'selenium-webdriver/firefox'; + +describe('start OpenSearch Dashboards server', () => { + let root: Root; + let config; + + // XPath Constants + const userIconBtnXPath = '//button[@id="user-icon-btn"]'; + const signInBtnXPath = '//*[@id="btn-sign-in"]'; + const skipWelcomeBtnXPath = '//button[@data-test-subj="skipWelcomeScreen"]'; + const tenantNameLabelXPath = '//*[@id="tenantName"]'; + const pageTitleXPath = '//*[@id="osdOverviewPageHeader__title"]'; + // Browser Settings + const browser = 'firefox'; + const options = new Options().headless(); + + beforeAll(async () => { + root = osdTestServer.createRootWithSettings( + { + plugins: { + scanDirs: [resolve(__dirname, '../..')], + }, + server: { + host: 'localhost', + port: 5601, + xsrf: { + whitelist: [ + '/_opendistro/_security/saml/acs/idpinitiated', + '/_opendistro/_security/saml/acs', + '/_opendistro/_security/saml/logout', + ], + }, + }, + logging: { + silent: true, + verbose: false, + }, + opensearch: { + hosts: ['https://localhost:9200'], + ignoreVersionMismatch: true, + ssl: { verificationMode: 'none' }, + username: OPENSEARCH_DASHBOARDS_SERVER_USER, + password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + requestHeadersWhitelist: ['authorization', 'securitytenant'], + }, + opensearch_security: { + auth: { + anonymous_auth_enabled: false, + type: 'saml', + }, + multitenancy: { + enabled: true, + tenants: { + enable_global: true, + enable_private: true, + 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(); + + await wreck.patch('https://localhost:9200/_plugins/_security/api/rolesmapping/all_access', { + payload: [ + { + op: 'add', + path: '/users', + value: ['saml.jackson@example.com'], + }, + ], + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }); + console.log('Starting to Download Flights Sample Data'); + await wreck.post('http://localhost:5601/api/sample_data/flights', { + payload: {}, + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + security_tenant: 'global', + }, + }); + console.log('Downloaded Sample Data'); + const getConfigResponse = await wreck.get( + 'https://localhost:9200/_plugins/_security/api/securityconfig', + { + rejectUnauthorized: false, + headers: { + authorization: ADMIN_CREDENTIALS, + }, + } + ); + const responseBody = (getConfigResponse.payload as Buffer).toString(); + config = JSON.parse(responseBody).config; + const samlConfig = { + http_enabled: true, + transport_enabled: false, + order: 5, + http_authenticator: { + challenge: true, + type: 'saml', + config: { + idp: { + metadata_url: 'http://localhost:7000/metadata', + entity_id: 'urn:example:idp', + }, + sp: { + entity_id: 'https://localhost:9200', + }, + kibana_url: 'http://localhost:5601', + exchange_key: '6aff3042-1327-4f3d-82f0-40a157ac4464', + }, + }, + authentication_backend: { + type: 'noop', + config: {}, + }, + }; + try { + config.dynamic!.authc!.saml_auth_domain = samlConfig; + config.dynamic!.authc!.basic_internal_auth_domain.http_authenticator.challenge = false; + config.dynamic!.http!.anonymous_auth_enabled = false; + await wreck.put('https://localhost:9200/_plugins/_security/api/securityconfig/config', { + payload: config, + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }); + } catch (error) { + console.log('Got an error while updating security config!!', error.stack); + fail(error); + } + }); + + afterAll(async () => { + console.log('Remove the Sample Data'); + await wreck + .delete('http://localhost:5601/api/sample_data/flights', { + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + console.log('Remove the Role Mapping'); + await wreck + .patch('https://localhost:9200/_plugins/_security/api/rolesmapping/all_access', { + payload: [ + { + op: 'remove', + path: '/users', + users: ['saml.jackson@example.com'], + }, + ], + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + console.log('Remove the Security Config'); + await wreck + .patch('https://localhost:9200/_plugins/_security/api/securityconfig', { + payload: [ + { + op: 'remove', + path: '/config/dynamic/authc/saml_auth_domain', + }, + ], + rejectUnauthorized: false, + headers: { + 'Content-Type': 'application/json', + authorization: ADMIN_CREDENTIALS, + }, + }) + .then((value) => { + Promise.resolve(value); + }) + .catch((value) => { + Promise.resolve(value); + }); + // shutdown OpenSearchDashboards server + await root.shutdown(); + }); + + it('Login to app/opensearch_dashboards_overview#/ when SAML is enabled', async () => { + const driver = getDriver(browser, options).build(); + await driver.get('http://localhost:5601/app/opensearch_dashboards_overview#/'); + await driver.findElement(By.id('btn-sign-in')).click(); + await driver.wait(until.elementsLocated(By.xpath(pageTitleXPath)), 10000); + + const cookie = await driver.manage().getCookies(); + expect(cookie.length).toEqual(2); + await driver.manage().deleteAllCookies(); + await driver.quit(); + }); + + it('Login to app/dev_tools#/console when SAML is enabled', async () => { + const driver = getDriver(browser, options).build(); + await driver.get('http://localhost:5601/app/dev_tools#/console'); + await driver.findElement(By.id('btn-sign-in')).click(); + + await driver.wait( + until.elementsLocated(By.xpath('//*[@data-test-subj="sendRequestButton"]')), + 10000 + ); + + const cookie = await driver.manage().getCookies(); + expect(cookie.length).toEqual(2); + await driver.manage().deleteAllCookies(); + await driver.quit(); + }); + + it('Login to Dashboard with Hash', async () => { + const urlWithHash = `http://localhost:5601/app/dashboards#/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)`; + const driver = getDriver(browser, options).build(); + await driver.manage().deleteAllCookies(); + await driver.get(urlWithHash); + await driver.findElement(By.xpath(signInBtnXPath)).click(); + // TODO Use a better XPath. + await driver.wait( + until.elementsLocated(By.xpath('/html/body/div[1]/div/header/div/div[2]')), + 20000 + ); + const windowHash = await driver.getCurrentUrl(); + expect(windowHash).toEqual(urlWithHash); + const cookie = await driver.manage().getCookies(); + expect(cookie.length).toEqual(2); + await driver.manage().deleteAllCookies(); + await driver.quit(); + }); + + it('Tenancy persisted after Logout in SAML', async () => { + const driver = getDriver(browser, options).build(); + + await driver.get('http://localhost:5601/app/opensearch_dashboards_overview#/'); + + await driver.findElement(By.xpath(signInBtnXPath)).click(); + + await driver.wait(until.elementsLocated(By.xpath(pageTitleXPath)), 10000); + + await driver.wait( + until.elementsLocated(By.xpath('//button[@aria-label="Closes this modal window"]')), + 10000 + ); + + // Select Global Tenant Radio Button + const radio = await driver.findElement(By.xpath('//input[@id="global"]')); + await driver.executeScript('arguments[0].scrollIntoView(true);', radio); + await driver.executeScript('arguments[0].click();', radio); + + await driver.findElement(By.xpath('//button[@data-test-subj="confirm"]')).click(); + + await driver.wait(until.elementsLocated(By.xpath(userIconBtnXPath)), 10000); + + await driver.findElement(By.xpath(userIconBtnXPath)).click(); + + await driver.findElement(By.xpath('//*[@data-test-subj="log-out-1"]')).click(); + + // RELOGIN AND CHECK TENANT + + await driver.wait(until.elementsLocated(By.xpath(signInBtnXPath)), 10000); + + await driver.findElement(By.xpath(signInBtnXPath)).click(); + + await driver.wait(until.elementsLocated(By.xpath(skipWelcomeBtnXPath)), 10000); + + await driver.findElement(By.xpath(skipWelcomeBtnXPath)).click(); + + await driver.findElement(By.xpath(userIconBtnXPath)).click(); + + await driver.wait(until.elementsLocated(By.xpath(tenantNameLabelXPath)), 10000); + + const tenantName = await driver.findElement(By.xpath(tenantNameLabelXPath)).getText(); + + await driver.manage().deleteAllCookies(); + await driver.quit(); + + expect(tenantName).toEqual('Global'); + }); +}); + +function getDriver(browser: string, options: Options) { + return new Builder().forBrowser(browser).setFirefoxOptions(options); +}