diff --git a/.gitignore b/.gitignore index 3a5dda1378c2e7..2739b9a50172ce 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,7 @@ fleet-server.yml **/.journeys/ x-pack/test/security_api_integration/plugins/audit_log/audit.log +# ignore FTR temp directory +.ftr +role_users.json diff --git a/packages/kbn-es/src/utils/docker.test.ts b/packages/kbn-es/src/utils/docker.test.ts index b574447a20508b..d877e5bdf8261c 100644 --- a/packages/kbn-es/src/utils/docker.test.ts +++ b/packages/kbn-es/src/utils/docker.test.ts @@ -462,9 +462,9 @@ describe('resolveEsArgs()', () => { "--env", "xpack.security.authc.realms.saml.mock-idp.attributes.groups=http://saml.elastic-cloud.com/attributes/roles", "--env", - "xpack.security.authc.realms.saml.mock-idp.attributes.name=http://saml.elastic-cloud.com/attributes/email", + "xpack.security.authc.realms.saml.mock-idp.attributes.name=http://saml.elastic-cloud.com/attributes/name", "--env", - "xpack.security.authc.realms.saml.mock-idp.attributes.mail=http://saml.elastic-cloud.com/attributes/name", + "xpack.security.authc.realms.saml.mock-idp.attributes.mail=http://saml.elastic-cloud.com/attributes/email", ] `); }); diff --git a/packages/kbn-es/src/utils/docker.ts b/packages/kbn-es/src/utils/docker.ts index 73e5e1fc772884..31c00beb7a71f1 100644 --- a/packages/kbn-es/src/utils/docker.ts +++ b/packages/kbn-es/src/utils/docker.ts @@ -508,11 +508,11 @@ export function resolveEsArgs( ); esArgs.set( `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name`, - MOCK_IDP_ATTRIBUTE_EMAIL + MOCK_IDP_ATTRIBUTE_NAME ); esArgs.set( `xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail`, - MOCK_IDP_ATTRIBUTE_NAME + MOCK_IDP_ATTRIBUTE_EMAIL ); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 4172e7087ea36f..cac8eec71e7f4d 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -6,22 +6,22 @@ * Side Public License, v 1. */ -import Url from 'url'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import { modifyUrl } from '@kbn/std'; import { cloneDeepWith, isString } from 'lodash'; import { Key, Origin, type WebDriver } from 'selenium-webdriver'; import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome'; -import { modifyUrl } from '@kbn/std'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import Url from 'url'; -import sharp from 'sharp'; import { NoSuchSessionError } from 'selenium-webdriver/lib/error'; +import sharp from 'sharp'; +import { FtrService, type FtrProviderContext } from '../../ftr_provider_context'; import { WebElementWrapper } from '../lib/web_element_wrapper'; -import { type FtrProviderContext, FtrService } from '../../ftr_provider_context'; import { Browsers } from '../remote/browsers'; import { + NETWORK_PROFILES, type NetworkOptions, type NetworkProfile, - NETWORK_PROFILES, } from '../remote/network_profiles'; export type Browser = BrowserService; @@ -246,6 +246,28 @@ class BrowserService extends FtrService { return await this.driver.get(url); } + /** + * Deletes all the cookies of the current browsing context. + * https://www.selenium.dev/documentation/webdriver/interactions/cookies/#delete-all-cookies + * + * @return {Promise} + */ + public async deleteAllCookies() { + await this.driver.manage().deleteAllCookies(); + } + + /** + * Adds a cookie to the current browsing context. You need to be on the domain that the cookie will be valid for. + * https://www.selenium.dev/documentation/webdriver/interactions/cookies/#add-cookie + * + * @param {string} name + * @param {string} value + * @return {Promise} + */ + public async setCookie(name: string, value: string) { + await this.driver.manage().addCookie({ name, value }); + } + /** * Retrieves the cookie with the given name. Returns null if there is no such cookie. The cookie will be returned as * a JSON object as described by the WebDriver wire protocol. @@ -258,6 +280,18 @@ class BrowserService extends FtrService { return await this.driver.manage().getCookie(cookieName); } + /** + * Returns a ‘successful serialized cookie data’ for current browsing context. + * If browser is no longer available it returns error. + * https://www.selenium.dev/documentation/webdriver/interactions/cookies/#get-all-cookies + * + * @param {string} cookieName + * @return {Promise} + */ + public async getCookies() { + return await this.driver.manage().getCookies(); + } + /** * Pauses the execution in the browser, similar to setting a breakpoint for debugging. * @return {Promise} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/index.ts index 8d5970aa843acf..86d4ad05cfc359 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/index.ts @@ -22,6 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./role_mappings')); loadTestFile(require.resolve('./sessions')); loadTestFile(require.resolve('./users')); + loadTestFile(require.resolve('./request_as_viewer')); loadTestFile(require.resolve('./user_profiles')); loadTestFile(require.resolve('./views')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/request_as_viewer.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/request_as_viewer.ts new file mode 100644 index 00000000000000..e04a98b4ff7e72 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/request_as_viewer.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + describe('security/request as viewer', () => { + const svlUserManager = getService('svlUserManager'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + let credentials: { Cookie: string }; + + before(async () => { + // get auth header for Viewer role + credentials = await svlUserManager.getApiCredentialsForRole('viewer'); + }); + + it('returns full status payload for authenticated request', async () => { + const { body } = await supertestWithoutAuth + .get('/api/status') + .set(credentials) + .set('kbn-xsrf', 'kibana'); + + expect(body.name).to.be.a('string'); + expect(body.uuid).to.be.a('string'); + expect(body.version.number).to.be.a('string'); + }); + }); +} diff --git a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts index 7762bf92d046ac..3709efe36ea1db 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts @@ -15,6 +15,9 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide const deployment = getService('deployment'); const log = getService('log'); const browser = getService('browser'); + const svlUserManager = getService('svlUserManager'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const svlCommonApi = getService('svlCommonApi'); const delay = (ms: number) => new Promise((resolve) => { @@ -22,13 +25,54 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide }); return { + async loginWithRole(role: string) { + await retry.waitForWithTimeout( + `Logging in by setting browser cookie for '${role}' role`, + 30_000, + async () => { + log.debug(`Delete all the cookies in the current browser context`); + await browser.deleteAllCookies(); + log.debug(`Setting the cookie for '${role}' role`); + const sidCookie = await svlUserManager.getSessionCookieForRole(role); + // Loading bootstrap.js in order to be on the domain that the cookie will be set for. + await browser.get(deployment.getHostPort() + '/bootstrap.js'); + await browser.setCookie('sid', sidCookie); + // Cookie should be already set in the browsing context, navigating to the Home page + await browser.get(deployment.getHostPort()); + // Verifying that we are logged in + if (await testSubjects.exists('userMenuButton', { timeout: 10_000 })) { + log.debug('userMenuButton found, login passed'); + } else { + throw new Error(`Failed to login with cookie for '${role}' role`); + } + + // Validating that the new cookie in the browser is set for the correct user + const browserCookies = await browser.getCookies(); + if (browserCookies.length === 0) { + throw new Error(`The cookie is missing in browser context`); + } + const { body } = await supertestWithoutAuth + .get('/internal/security/me') + .set(svlCommonApi.getInternalRequestHeader()) + .set('Cookie', `sid=${browserCookies[0].value}`); + + const userData = await svlUserManager.getUserData(role); + // email returned from API call must match the email for the specified role + if (body.email === userData.email) { + log.debug(`The new cookie is properly set for '${role}' role`); + return true; + } else { + throw new Error( + `Cookie is not set properly, expected email is '${userData.email}', but found '${body.email}'` + ); + } + } + ); + }, + async navigateToLoginForm() { const url = deployment.getHostPort() + '/login'; await browser.get(url); - // ensure welcome screen won't be shown. This is relevant for environments which don't allow - // to use the yml setting, e.g. cloud - await browser.setLocalStorageItem('home:welcome:show', 'false'); - log.debug('Waiting for Login Form to appear.'); await retry.waitForWithTimeout('login form', 10_000, async () => { return await pageObjects.security.isLoginFormVisible(); diff --git a/x-pack/test_serverless/functional/test_suites/common/platform_security/index.ts b/x-pack/test_serverless/functional/test_suites/common/platform_security/index.ts index bbb66a98418682..7427384fc73bf1 100644 --- a/x-pack/test_serverless/functional/test_suites/common/platform_security/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/platform_security/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Serverless Common UI - Platform Security', function () { + loadTestFile(require.resolve('./viewer_role_login')); loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./navigation/avatar_menu')); loadTestFile(require.resolve('./user_profiles/user_profiles')); diff --git a/x-pack/test_serverless/functional/test_suites/common/platform_security/viewer_role_login.ts b/x-pack/test_serverless/functional/test_suites/common/platform_security/viewer_role_login.ts new file mode 100644 index 00000000000000..767abe540c2f50 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/platform_security/viewer_role_login.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const VIEWER_ROLE = 'viewer'; + +export default function ({ getPageObject, getService }: FtrProviderContext) { + describe(`Login as ${VIEWER_ROLE}`, function () { + const svlCommonPage = getPageObject('svlCommonPage'); + const testSubjects = getService('testSubjects'); + const svlUserManager = getService('svlUserManager'); + + before(async () => { + await svlCommonPage.loginWithRole(VIEWER_ROLE); + }); + + it('should be able to see correct profile', async () => { + await svlCommonPage.assertProjectHeaderExists(); + await svlCommonPage.assertUserAvatarExists(); + await svlCommonPage.clickUserAvatar(); + await svlCommonPage.assertUserMenuExists(); + const actualFullname = await testSubjects.getVisibleText('contextMenuPanelTitle'); + const userData = await svlUserManager.getUserData(VIEWER_ROLE); + expect(actualFullname).to.be(userData.fullname); + }); + }); +} diff --git a/x-pack/test_serverless/shared/services/index.ts b/x-pack/test_serverless/shared/services/index.ts index a3168734b28eeb..3803ca84490e50 100644 --- a/x-pack/test_serverless/shared/services/index.ts +++ b/x-pack/test_serverless/shared/services/index.ts @@ -8,10 +8,12 @@ import { SvlReportingServiceProvider } from './svl_reporting'; import { SupertestProvider, SupertestWithoutAuthProvider } from './supertest'; import { SvlCommonApiServiceProvider } from './svl_common_api'; +import { SvlUserManagerProvider } from './user_manager/svl_user_manager'; export const services = { supertest: SupertestProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, svlCommonApi: SvlCommonApiServiceProvider, svlReportingApi: SvlReportingServiceProvider, + svlUserManager: SvlUserManagerProvider, }; diff --git a/x-pack/test_serverless/shared/services/supertest.ts b/x-pack/test_serverless/shared/services/supertest.ts index 1a07e540184894..dec306dcb8f284 100644 --- a/x-pack/test_serverless/shared/services/supertest.ts +++ b/x-pack/test_serverless/shared/services/supertest.ts @@ -9,21 +9,29 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; import { FtrProviderContext } from '../../functional/ftr_provider_context'; +/** + * Returns supertest.SuperTest instance that will not persist cookie between API requests. + */ export function SupertestProvider({ getService }: FtrProviderContext) { const config = getService('config'); const kbnUrl = formatUrl(config.get('servers.kibana')); - const ca = config.get('servers.kibana').certificateAuthorities; - return supertest.agent(kbnUrl, { ca }); + return supertest(kbnUrl); } +/** + * Returns supertest.SuperTest instance that will not persist cookie between API requests. + * If you need to pass certificate, do the following: + * await supertestWithoutAuth + * .get('/abc') + * .ca(CA_CERT) + */ export function SupertestWithoutAuthProvider({ getService }: FtrProviderContext) { const config = getService('config'); const kbnUrl = formatUrl({ ...config.get('servers.kibana'), auth: false, }); - const ca = config.get('servers.kibana').certificateAuthorities; - return supertest.agent(kbnUrl, { ca }); + return supertest(kbnUrl); } diff --git a/x-pack/test_serverless/shared/services/user_manager/saml_auth.ts b/x-pack/test_serverless/shared/services/user_manager/saml_auth.ts new file mode 100644 index 00000000000000..ac69ec402fa7c5 --- /dev/null +++ b/x-pack/test_serverless/shared/services/user_manager/saml_auth.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSAMLResponse as createMockedSAMLResponse } from '@kbn/mock-idp-plugin/common'; +import { ToolingLog } from '@kbn/tooling-log'; +import axios, { AxiosResponse } from 'axios'; +import * as cheerio from 'cheerio'; +import { parse as parseCookie } from 'tough-cookie'; +import Url from 'url'; +import { Session } from './svl_user_manager'; + +export interface CloudSamlSessionParams { + email: string; + password: string; + kbnHost: string; + kbnVersion: string; + log: ToolingLog; +} + +export interface LocalSamlSessionParams { + username: string; + email: string; + fullname: string; + role: string; + kbnHost: string; + log: ToolingLog; +} + +export interface CreateSamlSessionParams { + hostname: string; + email: string; + password: string; + log: ToolingLog; +} + +const cleanException = (url: string, ex: any) => { + if (ex.isAxiosError) { + ex.url = url; + if (ex.response?.data) { + if (ex.response.data?.message) { + ex.response_message = ex.response.data.message; + } else { + ex.data = ex.response.data; + } + } + ex.config = { REDACTED: 'REDACTED' }; + ex.request = { REDACTED: 'REDACTED' }; + ex.response = { REDACTED: 'REDACTED' }; + } +}; + +const getSessionCookie = (cookieString: string) => { + return parseCookie(cookieString); +}; + +const getCloudHostName = () => { + const hostname = process.env.TEST_CLOUD_HOST_NAME; + if (!hostname) { + throw new Error('SAML Authentication requires TEST_CLOUD_HOST_NAME env variable to be set'); + } + + return hostname; +}; + +const getCloudUrl = (hostname: string, pathname: string) => { + return Url.format({ + protocol: 'https', + hostname, + pathname, + }); +}; + +const createCloudSession = async (params: CreateSamlSessionParams) => { + const { hostname, email, password, log } = params; + const cloudLoginUrl = getCloudUrl(hostname, '/api/v1/users/_login'); + let sessionResponse: AxiosResponse; + try { + sessionResponse = await axios.request({ + url: cloudLoginUrl, + method: 'post', + data: { + email, + password, + }, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + validateStatus: () => true, + maxRedirects: 0, + }); + } catch (ex) { + log.error('Failed to create the new cloud session'); + cleanException(cloudLoginUrl, ex); + throw ex; + } + + const firstName = sessionResponse?.data?.user?.data?.first_name ?? ''; + const lastName = sessionResponse?.data?.user?.data?.last_name ?? ''; + const firstLastNames = `${firstName} ${lastName}`.trim(); + const fullname = firstLastNames.length > 0 ? firstLastNames : email; + const token = sessionResponse?.data?.token as string; + if (!token) { + log.error( + `Failed to create cloud session, token is missing in response data: ${JSON.stringify( + sessionResponse?.data + )}` + ); + throw new Error(`Unable to create Cloud session, token is missing.`); + } + return { token, fullname }; +}; + +const createSAMLRequest = async (kbnUrl: string, kbnVersion: string, log: ToolingLog) => { + let samlResponse: AxiosResponse; + const url = kbnUrl + '/internal/security/login'; + try { + samlResponse = await axios.request({ + url, + method: 'post', + data: { + providerType: 'saml', + providerName: 'cloud-saml-kibana', + currentURL: kbnUrl + '/login?next=%2F"', + }, + headers: { + 'kbn-version': kbnVersion, + 'x-elastic-internal-origin': 'Kibana', + 'content-type': 'application/json', + }, + validateStatus: () => true, + maxRedirects: 0, + }); + } catch (ex) { + log.error('Failed to create SAML request'); + cleanException(url, ex); + throw ex; + } + + const cookie = getSessionCookie(samlResponse.headers['set-cookie']![0]); + if (!cookie) { + throw new Error(`Failed to parse cookie from SAML response headers`); + } + + const location = samlResponse?.data?.location as string; + if (!location) { + throw new Error( + `Failed to get location from SAML response data: ${JSON.stringify(samlResponse.data)}` + ); + } + return { location, sid: cookie.value }; +}; + +const createSAMLResponse = async (url: string, ecSession: string) => { + const samlResponse = await axios.get(url, { + headers: { + Cookie: `ec_session=${ecSession}`, + }, + }); + const $ = cheerio.load(samlResponse.data); + const value = $('input').attr('value') ?? ''; + if (value.length === 0) { + throw new Error('Failed to parse SAML response value'); + } + return value; +}; + +const finishSAMLHandshake = async ({ + kbnHost, + samlResponse, + sid, + log, +}: { + kbnHost: string; + samlResponse: string; + sid?: string; + log: ToolingLog; +}) => { + const encodedResponse = encodeURIComponent(samlResponse); + const url = kbnHost + '/api/security/saml/callback'; + let authResponse: AxiosResponse; + + try { + authResponse = await axios.request({ + url, + method: 'post', + data: `SAMLResponse=${encodedResponse}`, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + ...(sid ? { Cookie: `sid=${sid}` } : {}), + }, + validateStatus: () => true, + maxRedirects: 0, + }); + } catch (ex) { + log.error('Failed to call SAML callback'); + cleanException(url, ex); + throw ex; + } + + const cookie = getSessionCookie(authResponse!.headers['set-cookie']![0]); + if (!cookie) { + throw new Error(`Failed to get cookie from SAML callback response headers`); + } + + return cookie; +}; + +export const createCloudSAMLSession = async (params: CloudSamlSessionParams) => { + const { email, password, kbnHost, kbnVersion, log } = params; + const hostname = getCloudHostName(); + const { token, fullname } = await createCloudSession({ hostname, email, password, log }); + const { location, sid } = await createSAMLRequest(kbnHost, kbnVersion, log); + const samlResponse = await createSAMLResponse(location, token); + const cookie = await finishSAMLHandshake({ kbnHost, samlResponse, sid, log }); + return new Session(cookie, email, fullname); +}; + +export const createLocalSAMLSession = async (params: LocalSamlSessionParams) => { + const { username, email, fullname, role, kbnHost, log } = params; + const samlResponse = await createMockedSAMLResponse({ + kibanaUrl: kbnHost + '/api/security/saml/callback', + username, + fullname, + email, + roles: [role], + }); + const cookie = await finishSAMLHandshake({ kbnHost, samlResponse, log }); + return new Session(cookie, email, fullname); +}; diff --git a/x-pack/test_serverless/shared/services/user_manager/svl_user_manager.ts b/x-pack/test_serverless/shared/services/user_manager/svl_user_manager.ts new file mode 100644 index 00000000000000..fd9560e7a7a80b --- /dev/null +++ b/x-pack/test_serverless/shared/services/user_manager/svl_user_manager.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import * as fs from 'fs'; +import { load as loadYaml } from 'js-yaml'; +import { resolve } from 'path'; +import { Cookie } from 'tough-cookie'; +import Url from 'url'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { createCloudSAMLSession, createLocalSAMLSession } from './saml_auth'; + +export interface User { + readonly email: string; + readonly password: string; +} + +export type Role = string; + +export class Session { + readonly cookie; + readonly email; + readonly fullname; + constructor(cookie: Cookie, email: string, fullname: string) { + this.cookie = cookie; + this.email = email; + this.fullname = fullname; + } + + getCookieValue() { + return this.cookie.value; + } +} + +export function SvlUserManagerProvider({ getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const config = getService('config'); + const log = getService('log'); + const isServerless = config.get('serverless'); + const isCloud = !!process.env.TEST_CLOUD; + const cloudRoleUsersFilePath = resolve(REPO_ROOT, '.ftr', 'role_users.json'); + const rolesDefinitionFilePath = resolve( + REPO_ROOT, + 'packages/kbn-es/src/serverless_resources/roles.yml' + ); + const roles: string[] = Object.keys(loadYaml(fs.readFileSync(rolesDefinitionFilePath, 'utf8'))); + const roleToUserMap: Map = new Map(); + + if (!isServerless) { + throw new Error(`'svlUserManager' service can't be used in non-serverless FTR context`); + } + + if (isCloud) { + // QAF should prepare the '.ftr/role_users.json' file for MKI pipelines + if (!fs.existsSync(cloudRoleUsersFilePath)) { + throw new Error( + `svlUserManager service requires user roles to be defined in ${cloudRoleUsersFilePath}` + ); + } + + const data = fs.readFileSync(cloudRoleUsersFilePath, 'utf8'); + if (data.length === 0) { + throw new Error(`'${cloudRoleUsersFilePath}' is empty: no roles are defined`); + } + for (const [roleName, user] of Object.entries(JSON.parse(data)) as Array<[string, User]>) { + roleToUserMap.set(roleName, user); + } + } + // to be re-used within FTR config run + const sessionCache = new Map(); + + const getCloudUserByRole = (role: string) => { + if (!roles.includes(role)) { + log.warning(`Role '${role}' is not listed in 'kbn-es/src/serverless_resources/roles.yml'`); + } + if (roleToUserMap.has(role)) { + return roleToUserMap.get(role)!; + } else { + throw new Error(`User with '${role}' role is not defined`); + } + }; + + const getSessionByRole = async (role: string) => { + if (sessionCache.has(role)) { + return sessionCache.get(role)!; + } + + const kbnHost = Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: isCloud ? undefined : config.get('servers.kibana.port'), + }); + let session: Session; + + if (isCloud) { + log.debug(`new SAML authentication with '${role}' role`); + const kbnVersion = await kibanaServer.version.get(); + session = await createCloudSAMLSession({ + ...getCloudUserByRole(role), + kbnHost, + kbnVersion, + log, + }); + } else { + log.debug(`new fake SAML authentication with '${role}' role`); + session = await createLocalSAMLSession({ + username: `elastic_${role}`, + email: `elastic_${role}@elastic.co`, + fullname: `test ${role}`, + role, + kbnHost, + log, + }); + } + + sessionCache.set(role, session); + return session; + }; + + return { + /* + * Returns auth header to do API calls with 'supertestWithoutAuth' service + * + * @example Create API call as a user with viewer role + * + * ```ts + * const credentials = await svlUserManager.getApiCredentialsForRole('viewer'); + * const response = await supertestWithoutAuth + * .get('/api/status') + * .set(credentials) + * .set('kbn-xsrf', 'kibana'); + * ``` + */ + async getApiCredentialsForRole(role: string) { + const session = await getSessionByRole(role); + return { Cookie: `sid=${session.getCookieValue()}` }; + }, + + /** + * Returns sid cookie that can be added to browser context for authentication + * + * @example Set cookie in browser context to login with specific role + * + * ```ts + * const sidCookie = await svlUserManager.getSessionCookieForRole(role); + * Loading bootstrap.js in order to be on the domain that the cookie will be set for. + * await browser.get(deployment.getHostPort() + '/bootstrap.js'); + * await browser.setCookie('sid', sidCookie); + * ``` + */ + async getSessionCookieForRole(role: string) { + const session = await getSessionByRole(role); + return session.getCookieValue(); + }, + + /** + * Returns SAML user email and full name + */ + async getUserData(role: string) { + const { email, fullname } = await getSessionByRole(role); + return { email, fullname }; + }, + }; +}