diff --git a/packages/kbn-test/index.ts b/packages/kbn-test/index.ts index 5be415161a4a59..1b092f1fdd25ef 100644 --- a/packages/kbn-test/index.ts +++ b/packages/kbn-test/index.ts @@ -13,7 +13,7 @@ export { startServersCli, startServers } from './src/functional_tests/start_serv // @internal export { runTestsCli, runTests } from './src/functional_tests/run_tests'; - +export { SamlSessionManager, type SamlSessionManagerOptions, type HostOptions } from './src/auth'; export { runElasticsearch, runKibanaServer } from './src/functional_tests/lib'; export { getKibanaCliArg, getKibanaCliLoggers } from './src/functional_tests/lib/kibana_cli_args'; diff --git a/packages/kbn-test/src/auth/helper.ts b/packages/kbn-test/src/auth/helper.ts new file mode 100644 index 00000000000000..d13e3ef69f37ba --- /dev/null +++ b/packages/kbn-test/src/auth/helper.ts @@ -0,0 +1,22 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as fs from 'fs'; +import { Role, User } from './types'; + +export const readCloudUsersFromFile = (filePath: string): Array<[Role, User]> => { + if (!fs.existsSync(filePath)) { + throw new Error(`Please define user roles with email/password in ${filePath}`); + } + const data = fs.readFileSync(filePath, 'utf8'); + if (data.length === 0) { + throw new Error(`'${filePath}' is empty: no roles are defined`); + } + + return Object.entries(JSON.parse(data)) as Array<[Role, User]>; +}; diff --git a/packages/kbn-test/src/auth/index.ts b/packages/kbn-test/src/auth/index.ts new file mode 100644 index 00000000000000..00631c3ab2b0a8 --- /dev/null +++ b/packages/kbn-test/src/auth/index.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + SamlSessionManager, + type SamlSessionManagerOptions, + type HostOptions, +} from './session_manager'; diff --git a/x-pack/test_serverless/shared/services/user_manager/saml_auth.ts b/packages/kbn-test/src/auth/saml_auth.ts similarity index 90% rename from x-pack/test_serverless/shared/services/user_manager/saml_auth.ts rename to packages/kbn-test/src/auth/saml_auth.ts index ac69ec402fa7c5..996f16eace38a4 100644 --- a/x-pack/test_serverless/shared/services/user_manager/saml_auth.ts +++ b/packages/kbn-test/src/auth/saml_auth.ts @@ -1,40 +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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ 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 { Cookie, 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; -} +import { CloudSamlSessionParams, CreateSamlSessionParams, LocalSamlSessionParams } from './types'; + +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; + } -export interface CreateSamlSessionParams { - hostname: string; - email: string; - password: string; - log: ToolingLog; + getCookieValue() { + return this.cookie.value; + } } const cleanException = (url: string, ex: any) => { diff --git a/packages/kbn-test/src/auth/session_manager.ts b/packages/kbn-test/src/auth/session_manager.ts new file mode 100644 index 00000000000000..2e498d2bc9c74b --- /dev/null +++ b/packages/kbn-test/src/auth/session_manager.ts @@ -0,0 +1,135 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { REPO_ROOT } from '@kbn/repo-info'; +import { ToolingLog } from '@kbn/tooling-log'; +import { resolve } from 'path'; +import Url from 'url'; +import { KbnClient } from '../kbn_client'; +import { readCloudUsersFromFile } from './helper'; +import { createCloudSAMLSession, createLocalSAMLSession, Session } from './saml_auth'; +import { Role, User } from './types'; + +export interface HostOptions { + protocol: 'http' | 'https'; + hostname: string; + port?: number; + username: string; + password: string; +} + +export interface SamlSessionManagerOptions { + hostOptions: HostOptions; + isCloud: boolean; + log: ToolingLog; +} + +/** + * Manages cookies associated with user roles + */ +export class SamlSessionManager { + private readonly isCloud: boolean; + private readonly kbnHost: string; + private readonly kbnClient: KbnClient; + private readonly log: ToolingLog; + private readonly roleToUserMap: Map; + private readonly sessionCache: Map; + private readonly userRoleFilePath = resolve(REPO_ROOT, '.ftr', 'role_users.json'); + + constructor(options: SamlSessionManagerOptions) { + this.isCloud = options.isCloud; + this.log = options.log; + const hostOptionsWithoutAuth = { + protocol: options.hostOptions.protocol, + hostname: options.hostOptions.hostname, + port: options.hostOptions.port, + }; + this.kbnHost = Url.format(hostOptionsWithoutAuth); + this.kbnClient = new KbnClient({ + log: this.log, + url: Url.format({ + ...hostOptionsWithoutAuth, + auth: `${options.hostOptions.username}:${options.hostOptions.password}`, + }), + }); + this.sessionCache = new Map(); + this.roleToUserMap = new Map(); + } + + /** + * Loads cloud users from '.ftr/role_users.json' + * QAF prepares the file for CI pipelines, make sure to add it manually for local run + */ + private getCloudUsers = () => { + if (this.roleToUserMap.size === 0) { + const data = readCloudUsersFromFile(this.userRoleFilePath); + for (const [roleName, user] of data) { + this.roleToUserMap.set(roleName, user); + } + } + + return this.roleToUserMap; + }; + + private getCloudUserByRole = (role: string) => { + if (this.getCloudUsers().has(role)) { + return this.getCloudUsers().get(role)!; + } else { + throw new Error(`User with '${role}' role is not defined`); + } + }; + + private getSessionByRole = async (role: string) => { + if (this.sessionCache.has(role)) { + return this.sessionCache.get(role)!; + } + + let session: Session; + + if (this.isCloud) { + this.log.debug(`new cloud SAML authentication with '${role}' role`); + const kbnVersion = await this.kbnClient.version.get(); + const { email, password } = this.getCloudUserByRole(role); + session = await createCloudSAMLSession({ + email, + password, + kbnHost: this.kbnHost, + kbnVersion, + log: this.log, + }); + } else { + this.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: this.kbnHost, + log: this.log, + }); + } + + this.sessionCache.set(role, session); + return session; + }; + + async getApiCredentialsForRole(role: string) { + const session = await this.getSessionByRole(role); + return { Cookie: `sid=${session.getCookieValue()}` }; + } + + async getSessionCookieForRole(role: string) { + const session = await this.getSessionByRole(role); + return session.getCookieValue(); + } + + async getUserData(role: string) { + const { email, fullname } = await this.getSessionByRole(role); + return { email, fullname }; + } +} diff --git a/packages/kbn-test/src/auth/sesson_manager.test.ts b/packages/kbn-test/src/auth/sesson_manager.test.ts new file mode 100644 index 00000000000000..88049acddb2e09 --- /dev/null +++ b/packages/kbn-test/src/auth/sesson_manager.test.ts @@ -0,0 +1,161 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { Cookie } from 'tough-cookie'; +import { Session } from './saml_auth'; +import { SamlSessionManager } from './session_manager'; +import * as samlAuth from './saml_auth'; +import * as helper from './helper'; +import { Role, User } from './types'; + +const log = new ToolingLog(); + +const cookieInstance = Cookie.parse( + 'sid=kbn_cookie_value; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT' +)!; +const email = 'testuser@elastic.com'; +const fullname = 'Test User'; + +const cloudCookieInstance = Cookie.parse( + 'sid=cloud_cookie_value; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT' +)!; +const cloudEmail = 'viewer@elastic.co'; +const cloudFullname = 'Test Viewer'; + +const cloudUsers = new Array<[Role, User]>(); +cloudUsers.push(['viewer', { email: 'viewer@elastic.co', password: 'p1234' }]); +cloudUsers.push(['admin', { email: 'admin@elastic.co', password: 'p1234' }]); + +const createLocalSAMLSessionMock = jest.spyOn(samlAuth, 'createLocalSAMLSession'); +const createCloudSAMLSessionMock = jest.spyOn(samlAuth, 'createCloudSAMLSession'); +const readCloudUsersFromFileMock = jest.spyOn(helper, 'readCloudUsersFromFile'); + +jest.mock('../kbn_client/kbn_client', () => { + return { + KbnClient: jest.fn(), + }; +}); +const get = jest.fn(); + +beforeEach(() => { + jest.resetAllMocks(); + jest + .requireMock('../kbn_client/kbn_client') + .KbnClient.mockImplementation(() => ({ version: { get } })); + get.mockImplementationOnce(() => Promise.resolve('8.12.0')); + + createLocalSAMLSessionMock.mockResolvedValue(new Session(cookieInstance, email, fullname)); + createCloudSAMLSessionMock.mockResolvedValue( + new Session(cloudCookieInstance, cloudEmail, cloudFullname) + ); + readCloudUsersFromFileMock.mockReturnValue(cloudUsers); +}); + +describe('SamlSessionManager', () => { + describe('for local session', () => { + const hostOptions = { + protocol: 'http' as 'http' | 'https', + hostname: 'localhost', + port: 5620, + username: 'elastic', + password: 'changeme', + }; + const isCloud = false; + test('should create an instance of SamlSessionManager', () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + expect(samlSessionManager).toBeInstanceOf(SamlSessionManager); + }); + + test(`'getSessionCookieForRole' should return the actual cookie value`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + const cookie = await samlSessionManager.getSessionCookieForRole('tester'); + expect(cookie).toBe(cookieInstance.value); + }); + + test(`'getApiCredentialsForRole' should return {Cookie: }`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + const credentials = await samlSessionManager.getApiCredentialsForRole('tester'); + expect(credentials).toEqual({ Cookie: `${cookieInstance.cookieString()}` }); + }); + + test(`'getSessionCookieForRole' should call 'createLocalSAMLSession' only once for the same role`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + await samlSessionManager.getSessionCookieForRole('tester'); + await samlSessionManager.getSessionCookieForRole('admin'); + await samlSessionManager.getSessionCookieForRole('tester'); + expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(2); + expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0); + }); + + test(`'getUserData' should return the correct email & fullname`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + const data = await samlSessionManager.getUserData('tester'); + expect(data).toEqual({ email, fullname }); + }); + }); + + describe('for cloud session', () => { + const hostOptions = { + protocol: 'https' as 'http' | 'https', + hostname: 'cloud', + username: 'elastic', + password: 'changeme', + }; + const isCloud = true; + test('should create an instance of SamlSessionManager', () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + expect(samlSessionManager).toBeInstanceOf(SamlSessionManager); + }); + + test(`'getSessionCookieForRole' should return the actual cookie value`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + createCloudSAMLSessionMock.mockResolvedValue( + new Session(cloudCookieInstance, cloudEmail, cloudFullname) + ); + const cookie = await samlSessionManager.getSessionCookieForRole('viewer'); + expect(cookie).toBe(cloudCookieInstance.value); + }); + + test(`'getApiCredentialsForRole' should return {Cookie: }`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + const credentials = await samlSessionManager.getApiCredentialsForRole('viewer'); + expect(credentials).toEqual({ Cookie: `${cloudCookieInstance.cookieString()}` }); + }); + + test(`'getSessionCookieForRole' should call 'createCloudSAMLSession' only once for the same role`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + await samlSessionManager.getSessionCookieForRole('viewer'); + await samlSessionManager.getSessionCookieForRole('admin'); + await samlSessionManager.getSessionCookieForRole('viewer'); + expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(0); + expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(2); + }); + + test(`'getUserData' should return the correct email & fullname`, async () => { + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + const data = await samlSessionManager.getUserData('viewer'); + expect(data).toEqual({ email: cloudEmail, fullname: cloudFullname }); + }); + + test(`throws error when roles does not exist`, async () => { + const nonExistingRole = 'tester'; + const samlSessionManager = new SamlSessionManager({ hostOptions, log, isCloud }); + await expect(samlSessionManager.getSessionCookieForRole(nonExistingRole)).rejects.toThrow( + `User with '${nonExistingRole}' role is not defined` + ); + await expect(samlSessionManager.getApiCredentialsForRole(nonExistingRole)).rejects.toThrow( + `User with '${nonExistingRole}' role is not defined` + ); + await expect(samlSessionManager.getUserData(nonExistingRole)).rejects.toThrow( + `User with '${nonExistingRole}' role is not defined` + ); + expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0); + }); + }); +}); diff --git a/packages/kbn-test/src/auth/types.ts b/packages/kbn-test/src/auth/types.ts new file mode 100644 index 00000000000000..45e5b78b0ba386 --- /dev/null +++ b/packages/kbn-test/src/auth/types.ts @@ -0,0 +1,40 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/tooling-log'; + +export interface CloudSamlSessionParams { + kbnHost: string; + kbnVersion: string; + email: string; + password: string; + log: ToolingLog; +} + +export interface LocalSamlSessionParams { + kbnHost: string; + email: string; + username: string; + fullname: string; + role: string; + log: ToolingLog; +} + +export interface CreateSamlSessionParams { + hostname: string; + email: string; + password: string; + log: ToolingLog; +} + +export interface User { + readonly email: string; + readonly password: string; +} + +export type Role = string; diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 7d73db67a0b926..abf0a35c4438ad 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -32,5 +32,6 @@ "@kbn/babel-register", "@kbn/repo-packages", "@kbn/core-saved-objects-api-server", + "@kbn/mock-idp-plugin", ] } 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 index e04a98b4ff7e72..7931f28f55df78 100644 --- 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 @@ -9,8 +9,9 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - describe('security/request as viewer', () => { + describe('security/me as viewer', () => { const svlUserManager = getService('svlUserManager'); + const svlCommonApi = getService('svlCommonApi'); const supertestWithoutAuth = getService('supertestWithoutAuth'); let credentials: { Cookie: string }; @@ -19,15 +20,17 @@ export default function ({ getService }: FtrProviderContext) { 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'); + it('returns valid user data for authenticated request', async () => { + const { body, status } = await supertestWithoutAuth + .get('/internal/security/me') + .set(svlCommonApi.getInternalRequestHeader()) + .set(credentials); - expect(body.name).to.be.a('string'); - expect(body.uuid).to.be.a('string'); - expect(body.version.number).to.be.a('string'); + const userData = await svlUserManager.getUserData('viewer'); + + expect(status).to.be(200); + expect(body.full_name).to.be(userData.fullname); + expect(body.email).to.be(userData.email); }); }); } 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 3709efe36ea1db..681f024b8f837f 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 @@ -54,7 +54,7 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide const { body } = await supertestWithoutAuth .get('/internal/security/me') .set(svlCommonApi.getInternalRequestHeader()) - .set('Cookie', `sid=${browserCookies[0].value}`); + .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 diff --git a/x-pack/test_serverless/shared/services/index.ts b/x-pack/test_serverless/shared/services/index.ts index 3803ca84490e50..90c94602133797 100644 --- a/x-pack/test_serverless/shared/services/index.ts +++ b/x-pack/test_serverless/shared/services/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { SvlReportingServiceProvider } from './svl_reporting'; import { SupertestProvider, SupertestWithoutAuthProvider } from './supertest'; import { SvlCommonApiServiceProvider } from './svl_common_api'; -import { SvlUserManagerProvider } from './user_manager/svl_user_manager'; +import { SvlReportingServiceProvider } from './svl_reporting'; +import { SvlUserManagerProvider } from './svl_user_manager'; export const services = { supertest: SupertestProvider, diff --git a/x-pack/test_serverless/shared/services/svl_user_manager.ts b/x-pack/test_serverless/shared/services/svl_user_manager.ts new file mode 100644 index 00000000000000..042cee2ffed3e3 --- /dev/null +++ b/x-pack/test_serverless/shared/services/svl_user_manager.ts @@ -0,0 +1,30 @@ +/* + * 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 { SamlSessionManager } from '@kbn/test'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +export function SvlUserManagerProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const log = getService('log'); + const isCloud = !!process.env.TEST_CLOUD; + + // Sharing the instance within FTR config run means cookies are persistent for each role between tests. + const sessionManager = new SamlSessionManager({ + hostOptions: { + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: isCloud ? undefined : config.get('servers.kibana.port'), + username: config.get('servers.kibana.username'), + password: config.get('servers.kibana.password'), + }, + log, + isCloud, + }); + + return sessionManager; +} 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 deleted file mode 100644 index fd9560e7a7a80b..00000000000000 --- a/x-pack/test_serverless/shared/services/user_manager/svl_user_manager.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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 }; - }, - }; -}