From 7608d76ac3b7688bea295b206a0ad075a2c69fb8 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Wed, 6 Nov 2024 13:09:07 +0100 Subject: [PATCH] [kbn-test] add forceNewSession option to re-generate session cookie (#199018) ## Summary Currently FTR caches `Cookies` of Cloud SAML sessions for the complete FTR config run, meaning we only perform actual login once for the specified role. It helps to optimise tests run time and improve stability. While it works most of the time, according to https://github.com/elastic/kibana/issues/71866 Reporting test suite stability depends on token validity (`20m`) and to stabilize it, this PR adds `forceNewSession` option to force request a new SAML session when it is required for specific tests. ``` cookieCredentials = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin', { forceNewSession: true, }); ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../services/saml_auth/saml_auth_provider.ts | 35 +++++++++-- packages/kbn-test/index.ts | 7 ++- packages/kbn-test/src/auth/index.ts | 3 +- .../kbn-test/src/auth/session_manager.test.ts | 59 ++++++++++++++++++ packages/kbn-test/src/auth/session_manager.ts | 61 +++++++++++++------ packages/kbn-test/src/auth/types.ts | 5 ++ .../common/reporting/datastream.ts | 6 +- .../common/reporting/generate_csv_discover.ts | 4 +- 8 files changed, 149 insertions(+), 31 deletions(-) diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts index efc86f85213c0..c317645cc921b 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { SamlSessionManager } from '@kbn/test'; +import { GetCookieOptions, SamlSessionManager } from '@kbn/test'; import expect from '@kbn/expect'; import { REPO_ROOT } from '@kbn/repo-info'; import { resolve } from 'path'; @@ -91,16 +91,39 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { }; return { - async getInteractiveUserSessionCookieWithRoleScope(role: string) { + /** + * Returns a Cookie string containing the session token for the specified role. + * This string can be used to update browser cookies and login with the designated role. + * + * @param role - The SAML role for which the session token is required. + * @param options - Optional settings to control session behavior, such as forcing a new session. + * @returns A string with the Cookie token + * + * @throws If the specified role is a custom role without a predefined descriptor. + */ + async getInteractiveUserSessionCookieWithRoleScope(role: string, options?: GetCookieOptions) { // Custom role has no descriptors by default, check if it was added before authentication throwIfRoleNotSet(role, CUSTOM_ROLE, supportedRoleDescriptors); - return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role); + return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role, options); }, - async getM2MApiCookieCredentialsWithRoleScope(role: string): Promise { + /** + * Returns an object containing a Cookie header with the session token for the specified role. + * This header can be used for authenticating API requests as the designated role. + * + * @param role - The SAML role for which the session token is required. + * @param options - Optional settings to control session behavior, such as forcing a new session. + * @returns An object with the Cookie header for API authentication. + * + * @throws If the specified role is a custom role without a predefined descriptor. + */ + async getM2MApiCookieCredentialsWithRoleScope( + role: string, + options?: GetCookieOptions + ): Promise { // Custom role has no descriptors by default, check if it was added before authentication throwIfRoleNotSet(role, CUSTOM_ROLE, supportedRoleDescriptors); - return sessionManager.getApiCredentialsForRole(role); + return sessionManager.getApiCredentialsForRole(role, options); }, async getEmail(role: string) { @@ -195,7 +218,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { expect(status).to.be(204); - // Update descriptors for custome role, it will be used to create API key + // Update descriptors for the custom role, it will be used to create API key supportedRoleDescriptors.set(CUSTOM_ROLE, customRoleDescriptors); }, diff --git a/packages/kbn-test/index.ts b/packages/kbn-test/index.ts index 3c03a32efa9ea..57d9c767827df 100644 --- a/packages/kbn-test/index.ts +++ b/packages/kbn-test/index.ts @@ -14,7 +14,12 @@ 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 { + SamlSessionManager, + type SamlSessionManagerOptions, + type HostOptions, + type GetCookieOptions, +} 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/index.ts b/packages/kbn-test/src/auth/index.ts index 61dd5853873b3..b16ddc6951944 100644 --- a/packages/kbn-test/src/auth/index.ts +++ b/packages/kbn-test/src/auth/index.ts @@ -9,6 +9,7 @@ export { SamlSessionManager, - type SamlSessionManagerOptions, + type GetCookieOptions, type HostOptions, + type SamlSessionManagerOptions, } from './session_manager'; diff --git a/packages/kbn-test/src/auth/session_manager.test.ts b/packages/kbn-test/src/auth/session_manager.test.ts index 4b20581eced4c..284432574833f 100644 --- a/packages/kbn-test/src/auth/session_manager.test.ts +++ b/packages/kbn-test/src/auth/session_manager.test.ts @@ -8,6 +8,7 @@ */ import { ToolingLog } from '@kbn/tooling-log'; +import crypto from 'crypto'; import { Cookie } from 'tough-cookie'; import { Session } from './saml_auth'; import { SamlSessionManager, SupportedRoles } from './session_manager'; @@ -33,6 +34,8 @@ const getSecurityProfileMock = jest.spyOn(samlAuth, 'getSecurityProfile'); const readCloudUsersFromFileMock = jest.spyOn(helper, 'readCloudUsersFromFile'); const isValidHostnameMock = jest.spyOn(helper, 'isValidHostname'); +const getTestToken = () => 'kbn_cookie_' + crypto.randomBytes(16).toString('hex'); + jest.mock('../kbn_client/kbn_client', () => { return { KbnClient: jest.fn(), @@ -105,6 +108,34 @@ describe('SamlSessionManager', () => { expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0); }); + test(`'getSessionCookieForRole' should call 'createLocalSAMLSession' again if 'forceNewSession = true'`, async () => { + const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions); + createLocalSAMLSessionMock.mockResolvedValueOnce( + new Session( + Cookie.parse(`sid=${getTestToken()}; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT`)!, + testEmail + ) + ); + const cookieStr1 = await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope( + roleViewer + ); + createLocalSAMLSessionMock.mockResolvedValueOnce( + new Session( + Cookie.parse(`sid=${getTestToken()}; Path=/; Expires=Wed, 01 Oct 2023 08:00:00 GMT`)!, + testEmail + ) + ); + const cookieStr2 = await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope( + roleViewer, + { + forceNewSession: true, + } + ); + expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(2); + expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(0); + expect(cookieStr1).not.toEqual(cookieStr2); + }); + test(`'getEmail' return the correct email`, async () => { const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions); const email = await samlSessionManager.getEmail(roleEditor); @@ -255,6 +286,34 @@ describe('SamlSessionManager', () => { expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(2); }); + test(`'getSessionCookieForRole' should call 'createCloudSAMLSession' again if 'forceNewSession = true'`, async () => { + const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions); + createCloudSAMLSessionMock.mockResolvedValueOnce( + new Session( + Cookie.parse(`sid=${getTestToken()}; Path=/; Expires=Wed, 01 Oct 2023 07:00:00 GMT`)!, + cloudEmail + ) + ); + const cookieStr1 = await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope( + roleViewer + ); + createCloudSAMLSessionMock.mockResolvedValueOnce( + new Session( + Cookie.parse(`sid=${getTestToken()}; Path=/; Expires=Wed, 01 Oct 2023 08:00:00 GMT`)!, + cloudEmail + ) + ); + const cookieStr2 = await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope( + roleViewer, + { + forceNewSession: true, + } + ); + expect(createLocalSAMLSessionMock.mock.calls).toHaveLength(0); + expect(createCloudSAMLSessionMock.mock.calls).toHaveLength(2); + expect(cookieStr1).not.toEqual(cookieStr2); + }); + test(`'getEmail' return the correct email`, async () => { const samlSessionManager = new SamlSessionManager(samlSessionManagerOptions); const email = await samlSessionManager.getEmail(roleViewer); diff --git a/packages/kbn-test/src/auth/session_manager.ts b/packages/kbn-test/src/auth/session_manager.ts index ba411aaa21891..4efd55a71aad5 100644 --- a/packages/kbn-test/src/auth/session_manager.ts +++ b/packages/kbn-test/src/auth/session_manager.ts @@ -17,7 +17,7 @@ import { getSecurityProfile, Session, } from './saml_auth'; -import { Role, User } from './types'; +import { GetSessionByRole, Role, User } from './types'; export interface HostOptions { protocol: 'http' | 'https'; @@ -40,6 +40,10 @@ export interface SupportedRoles { roles: string[]; } +export interface GetCookieOptions { + forceNewSession: boolean; +} + /** * Manages cookies associated with user roles */ @@ -115,24 +119,32 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment` } }; - private getSessionByRole = async (role: string) => { - if (this.sessionCache.has(role)) { + private getSessionByRole = async (options: GetSessionByRole): Promise => { + const { role, forceNewSession } = options; + + // Validate role before creating SAML session + this.validateRole(role); + + // Check if session is cached and not forced to create the new one + if (!forceNewSession && this.sessionCache.has(role)) { return this.sessionCache.get(role)!; } - // Validate role before creating SAML session - if (this.supportedRoles && !this.supportedRoles.roles.includes(role)) { - throw new Error( - `Role '${role}' is not in the supported list: ${this.supportedRoles.roles.join( - ', ' - )}. Add role descriptor in ${this.supportedRoles.sourcePath} to enable it for testing` - ); + const session = await this.createSessionForRole(role); + this.sessionCache.set(role, session); + + if (forceNewSession) { + this.log.debug(`Session for role '${role}' was force updated.`); } + return session; + }; + + private createSessionForRole = async (role: string): Promise => { let session: Session; if (this.isCloud) { - this.log.debug(`new cloud SAML authentication with '${role}' role`); + this.log.debug(`Creating new cloud SAML session for role '${role}'`); const kbnVersion = await this.kbnClient.version.get(); const { email, password } = this.getCloudUserByRole(role); session = await createCloudSAMLSession({ @@ -143,7 +155,7 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment` log: this.log, }); } else { - this.log.debug(`new fake SAML authentication with '${role}' role`); + this.log.debug(`Creating new local SAML session for role '${role}'`); session = await createLocalSAMLSession({ username: `elastic_${role}`, email: `elastic_${role}@elastic.co`, @@ -154,27 +166,38 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment` }); } - this.sessionCache.set(role, session); return session; }; - async getApiCredentialsForRole(role: string) { - const session = await this.getSessionByRole(role); + private validateRole = (role: string): void => { + if (this.supportedRoles && !this.supportedRoles.roles.includes(role)) { + throw new Error( + `Role '${role}' is not in the supported list: ${this.supportedRoles.roles.join( + ', ' + )}. Add role descriptor in ${this.supportedRoles.sourcePath} to enable it for testing` + ); + } + }; + + async getApiCredentialsForRole(role: string, options?: GetCookieOptions) { + const { forceNewSession } = options || { forceNewSession: false }; + const session = await this.getSessionByRole({ role, forceNewSession }); return { Cookie: `sid=${session.getCookieValue()}` }; } - async getInteractiveUserSessionCookieWithRoleScope(role: string) { - const session = await this.getSessionByRole(role); + async getInteractiveUserSessionCookieWithRoleScope(role: string, options?: GetCookieOptions) { + const { forceNewSession } = options || { forceNewSession: false }; + const session = await this.getSessionByRole({ role, forceNewSession }); return session.getCookieValue(); } async getEmail(role: string) { - const session = await this.getSessionByRole(role); + const session = await this.getSessionByRole({ role, forceNewSession: false }); return session.email; } async getUserData(role: string) { - const { cookie } = await this.getSessionByRole(role); + const { cookie } = await this.getSessionByRole({ role, forceNewSession: false }); const profileData = await getSecurityProfile({ kbnHost: this.kbnHost, cookie, log: this.log }); return profileData; } diff --git a/packages/kbn-test/src/auth/types.ts b/packages/kbn-test/src/auth/types.ts index 170793b8950a1..4a61f71d5d572 100644 --- a/packages/kbn-test/src/auth/types.ts +++ b/packages/kbn-test/src/auth/types.ts @@ -61,3 +61,8 @@ export interface RetryParams { attemptsCount: number; attemptDelay: number; } + +export interface GetSessionByRole { + role: string; + forceNewSession: boolean; +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts b/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts index a4eb181e6bc2b..325f053134a67 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/reporting/datastream.ts @@ -33,12 +33,12 @@ export default function ({ getService }: FtrProviderContext) { }; describe('Data Stream', function () { - // see details: https://github.com/elastic/kibana/issues/198811 - this.tags(['failsOnMKI']); const generatedReports = new Set(); before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - cookieCredentials = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + cookieCredentials = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin', { + forceNewSession: true, + }); internalReqHeader = svlCommonApi.getInternalRequestHeader(); await esArchiver.load(archives.ecommerce.data); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts b/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts index c654e5e307f86..3ab3037bdb359 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts @@ -79,7 +79,9 @@ export default function ({ getService }: FtrProviderContext) { this.timeout(12 * 60 * 1000); before(async () => { - cookieCredentials = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + cookieCredentials = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin', { + forceNewSession: true, + }); internalReqHeader = svlCommonApi.getInternalRequestHeader(); });