From c580213c00d6ffa148b7cdb49d3f74e77524fc1a Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Wed, 13 Dec 2023 17:12:57 +0100 Subject: [PATCH] [Security Solution] [Cypress] SAML for Serverless login and role testing (#172655) Relates to: * https://github.com/elastic/kibana/issues/166340 * https://github.com/elastic/kibana/pull/170852 * https://github.com/elastic/kibana/pull/170417 * https://github.com/elastic/kibana/pull/172678 ## Summary In this PR we are using the code implemented on https://github.com/elastic/kibana/pull/170417 and https://github.com/elastic/kibana/pull/172678 to allow SAML and role testing inside Cypress. * We are creating a Cypress task to use the above-developed code and be able to retrieve a session cookie given a role. * We updated the login task to know how we should perform the login depending if we are in Serverless (MKI or serverless FTR) or ESS * In the parallel serverless script: * We are updating the `BASE_ENV_URL` variable to use the proper QA environment (pending to be done in follow-up PRs, to extract this value so it is not hardcoded cc @dkirchan ) * We are adding the `IS_SERVERLESS` environment variable needed for the logic on the login task. This changed implied to update the `es_archiver` file to continue work as expected. * We have added the `TEST_CLOUD_HOST_NAME` environment variable needed for the code we are reusing to retrieve the session cookie for MKI. * We have updated the Security Solution quality gate script to set the `role_users.json` file needed by the code we are reusing to get the different session cookies on MKI * We have adjusted the tests because the username now follows the pattern `test ` (@dmlemeshko is it possible to have as username just the role? Is this something that can impact other tests and teams?) * We have [skipped](https://github.com/elastic/kibana/issues/173168) a test that got unstable after the changes. ## How to test it in your machine ### Serverless FTR 1. Navigate to `x-pack/test/security_solution_cypress` 2. Execute `yarn cypress:open:qa:serverless` 3. Click on `E2E testing` 4. Click on any test to execute it ### Serverless MKI Setup a valid Elastic Cloud API key for QA environment: 1. Navigate to QA environment. 2. Click on the `User menu button` located on the top right of the header. 3. Click on `Organization`. 5. Click on the `API keys` tab. 6. Click on `Create API key` button. 7. Add a name, set an expiration date, assign an organization owner role. 8. Click on `Create API key` 9. Save the value of the key Store the saved key on `~/.elastic/cloud.json` using the following format: ```json { "api_key": { "qa": "" } } ``` Store the email and password of the account you used to login in the QA Environment at the root directory of your Kibana project on `.ftr/role_users.json`, using the following format: ```json { "admin": { "email": "", "password": "" } } ``` If you want to execute a test with a role different from the default one, make sure you have created the user under your organization and is added to the above json following the format: ```json { "admin": { "email": "", "password": "" }, "": { "email": "", "password": "" } } ``` 1. Navigate to `x-pack/test/security_solution_cypress` 2. Execute `yarn cypress:open:qa:serverless` 3. Click on `E2E testing` 4. Click on any test to execute it --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../mki_security_solution_cypress.sh | 3 ++ .../run_cypress/parallel_serverless.ts | 4 +- .../cypress/README.md | 47 ++++++++++++++++++- .../cypress/cypress_ci_serverless.config.ts | 2 + .../cypress_ci_serverless_qa.config.ts | 2 + .../cypress/cypress_serverless.config.ts | 2 + .../cypress/e2e/explore/cases/creation.cy.ts | 4 +- .../e2e/explore/overview/overview.cy.ts | 3 +- .../cypress/screens/alerts.ts | 11 ++++- .../cypress/support/es_archiver.ts | 4 +- .../cypress/support/saml_auth.ts | 41 ++++++++++++++++ .../cypress/support/setup_users.ts | 2 +- .../cypress/tasks/login.ts | 18 ++++++- .../cypress/tsconfig.json | 2 +- 14 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/cypress/support/saml_auth.ts diff --git a/.buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh b/.buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh index 56b44315bf0642..9d353b167ea472 100755 --- a/.buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh +++ b/.buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh @@ -16,6 +16,9 @@ export JOB=kibana-security-solution-chrome buildkite-agent meta-data set "${BUILDKITE_JOB_ID}_is_test_execution_step" "true" +mkdir .ftr +retry 5 5 vault kv get -format=json -field=data secret/kibana-issues/dev/security-quality-gate/role-users > .ftr/role_users.json + cd x-pack/test/security_solution_cypress set +e diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts index 22b51692eb33fa..f3a7070aa7e331 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts @@ -61,7 +61,7 @@ const DEFAULT_CONFIGURATION: Readonly = [ const DEFAULT_REGION = 'aws-eu-west-1'; const PROJECT_NAME_PREFIX = 'kibana-cypress-security-solution-ephemeral'; -const BASE_ENV_URL = 'https://global.qa.cld.elstc.co'; +const BASE_ENV_URL = 'https://console.qa.cld.elstc.co'; let log: ToolingLog; const API_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress-creds', @@ -571,6 +571,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} KIBANA_PASSWORD: credentials.password, CLOUD_SERVERLESS: true, + IS_SERVERLESS: true, }; if (process.env.DEBUG && !process.env.CI) { @@ -582,6 +583,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} ---------------------------------------------- `); } + process.env.TEST_CLOUD_HOST_NAME = new URL(BASE_ENV_URL).hostname; if (isOpen) { await cypress.open({ diff --git a/x-pack/test/security_solution_cypress/cypress/README.md b/x-pack/test/security_solution_cypress/cypress/README.md index 16a93f0c3b5d52..0fa6bae6e2164a 100644 --- a/x-pack/test/security_solution_cypress/cypress/README.md +++ b/x-pack/test/security_solution_cypress/cypress/README.md @@ -304,8 +304,51 @@ Store the saved key on `~/.elastic/cloud.json` using the following format: } ``` -#### Known limitations -- Currently RBAC cannot be tested. +Store the email and password of the account you used to login in the QA Environment at the root directory of your Kibana project on `.ftr/role_users.json`, using the following format: + +```json +{ + "admin": { + "email": "", + "password": "" + } +} +``` + +#### Testing with different roles + +If you want to execute a test using Cypress on visual mode with MKI, you need to make sure you have the user created in your organization, and add it tot he `.ftr/role_users.json`: + +```json +{ + "admin": { + "email": "", + "password": "" + }, + "": { + "email": "", + "password": "" + } +} +``` + +As role names please use: +- admin +- detections_admin +- editor +- endpoint_operations_analyst +- endpoint_policy_manager +- none +- platform_engineer +- rule_author +- soc_manager +- t1_analyst +- t2_analyst +- t3_analyst +- threat_intelligence_analyst +- viewer + +The above should be the same used on the automation. #### PLIs diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts index 3a1be3ed0221ab..62aec8c49e7874 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts @@ -7,6 +7,7 @@ import { defineCypressConfig } from '@kbn/cypress-config'; import { esArchiver } from './support/es_archiver'; +import { samlAuthentication } from './support/saml_auth'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ @@ -39,6 +40,7 @@ export default defineCypressConfig({ specPattern: './cypress/e2e/**/*.cy.ts', setupNodeEvents(on, config) { esArchiver(on, config); + samlAuthentication(on, config); // eslint-disable-next-line @typescript-eslint/no-var-requires require('@cypress/grep/src/plugin')(config); return config; diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts index 342c3da34bef6e..c88faf0d9cfe37 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts @@ -7,6 +7,7 @@ import { defineCypressConfig } from '@kbn/cypress-config'; import { esArchiver } from './support/es_archiver'; +import { samlAuthentication } from './support/saml_auth'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ @@ -41,6 +42,7 @@ export default defineCypressConfig({ specPattern: './cypress/e2e/**/*.cy.ts', setupNodeEvents(on, config) { esArchiver(on, config); + samlAuthentication(on, config); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // eslint-disable-next-line @typescript-eslint/no-var-requires require('@cypress/grep/src/plugin')(config); diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_serverless.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_serverless.config.ts index b925e18a834788..b76c7ff22bcbcd 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_serverless.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_serverless.config.ts @@ -7,6 +7,7 @@ import { defineCypressConfig } from '@kbn/cypress-config'; import { esArchiver } from './support/es_archiver'; +import { samlAuthentication } from './support/saml_auth'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ @@ -31,6 +32,7 @@ export default defineCypressConfig({ experimentalMemoryManagement: true, setupNodeEvents(on, config) { esArchiver(on, config); + samlAuthentication(on, config); // eslint-disable-next-line @typescript-eslint/no-var-requires require('@cypress/grep/src/plugin')(config); return config; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts index 8891a835b1fd7a..698a97df6b5d6b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts @@ -106,10 +106,10 @@ describe('Cases', { tags: ['@ess', '@serverless'] }, () => { ); cy.get(CASE_DETAILS_USERNAMES) .eq(REPORTER) - .should('have.text', Cypress.env(ELASTICSEARCH_USERNAME)); + .should('contain', Cypress.env(ELASTICSEARCH_USERNAME)); cy.get(CASE_DETAILS_USERNAMES) .eq(PARTICIPANTS) - .should('have.text', Cypress.env(ELASTICSEARCH_USERNAME)); + .should('contain', Cypress.env(ELASTICSEARCH_USERNAME)); cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags); EXPECTED_METRICS.forEach((metric) => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/overview/overview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/overview/overview.cy.ts index f8578054628772..b0b1a11bc69e0c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/overview/overview.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/overview/overview.cy.ts @@ -46,7 +46,8 @@ describe('Overview Page', { tags: ['@ess', '@serverless'] }, () => { }); }); - describe('Favorite Timelines', () => { + // https://github.com/elastic/kibana/issues/173168 + describe('Favorite Timelines', { tags: ['@brokenInServerless'] }, () => { it('should appear on overview page', () => { createTimeline(getTimeline()) .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts index 0f1a513fc5d86b..83de06466f251d 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IS_SERVERLESS, CLOUD_SERVERLESS } from '../env_var_names_constants'; import { getDataTestSubjectSelector } from '../helpers/common'; import { GLOBAL_FILTERS_CONTAINER } from './date_picker'; @@ -204,9 +205,15 @@ export const ALERT_ASSIGNEES_SELECT_PANEL = export const ALERT_ASSIGNEES_UPDATE_BUTTON = '[data-test-subj="securitySolutionAssigneesApplyButton"]'; -export const ALERT_USER_AVATAR = (assignee: string) => - `[data-test-subj="securitySolutionUsersAvatar-${assignee}"][title='${assignee}']`; +export const ALERT_USER_AVATAR = (assignee: string) => { + let expectedAssignee = assignee; + if (Cypress.env(IS_SERVERLESS) && !Cypress.env(CLOUD_SERVERLESS)) { + expectedAssignee = `test ${expectedAssignee}`; + } + + return `[data-test-subj^="securitySolutionUsersAvatar-"][title='${expectedAssignee}']`; +}; export const ALERT_AVATARS_PANEL = '[data-test-subj="securitySolutionUsersAvatarsPanel"]'; export const ALERT_ASIGNEES_COLUMN = diff --git a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts index c9f271265a1d11..a5149b9df1e12b 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/es_archiver.ts @@ -18,7 +18,7 @@ export const esArchiver = ( ): EsArchiver => { const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout }); - const isSnapshotServerless = config.env.IS_SERVERLESS; + const isServerless = config.env.IS_SERVERLESS; const isCloudServerless = config.env.CLOUD_SERVERLESS; const serverlessCloudUser = { @@ -27,7 +27,7 @@ export const esArchiver = ( }; let authOverride; - if (!isSnapshotServerless) { + if (isServerless) { authOverride = isCloudServerless ? serverlessCloudUser : systemIndicesSuperuser; } diff --git a/x-pack/test/security_solution_cypress/cypress/support/saml_auth.ts b/x-pack/test/security_solution_cypress/cypress/support/saml_auth.ts new file mode 100644 index 00000000000000..0026f6c91ec27f --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/support/saml_auth.ts @@ -0,0 +1,41 @@ +/* + * 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 { ToolingLog } from '@kbn/tooling-log'; + +import { SecurityRoleName } from '@kbn/security-solution-plugin/common/test'; +import { HostOptions, SamlSessionManager } from '@kbn/test'; + +export const samlAuthentication = async ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): Promise => { + const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout }); + + const kbnHost = config.env.KIBANA_URL || config.env.BASE_URL; + + const kbnUrl = new URL(kbnHost); + + const hostOptions: HostOptions = { + protocol: kbnUrl.protocol as 'http' | 'https', + hostname: kbnUrl.hostname, + port: parseInt(kbnUrl.port, 10), + username: config.env.ELASTICSEARCH_USERNAME, + password: config.env.ELASTICSEARCH_PASSWORD, + }; + + on('task', { + getSessionCookie: async (role: string | SecurityRoleName): Promise => { + const sessionManager = new SamlSessionManager({ + hostOptions, + log, + isCloud: config.env.CLOUD_SERVERLESS, + }); + return sessionManager.getSessionCookieForRole(role); + }, + }); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/support/setup_users.ts b/x-pack/test/security_solution_cypress/cypress/support/setup_users.ts index 02ebebb6c10eab..73d17b26ab93b7 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/setup_users.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/setup_users.ts @@ -39,7 +39,7 @@ function createUser(username: string, password: string, roles: string[] = []): v password, roles, full_name: username, - email: '', + email: `${username}@elastic.co`, }; rootRequest({ diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/login.ts b/x-pack/test/security_solution_cypress/cypress/tasks/login.ts index fb2a93770f8e7d..90c9d245c3bbab 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/login.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/login.ts @@ -41,8 +41,22 @@ export const getEnvAuth = (role: SecurityRoleName): User => { }; export const login = (role?: SecurityRoleName): void => { - const user = role ? getEnvAuth(role) : defaultUser; - loginWithUser(user); + let testRole = ''; + + if (Cypress.env(IS_SERVERLESS)) { + if (!role) { + testRole = Cypress.env(CLOUD_SERVERLESS) ? 'admin' : 'system_indices_superuser'; + } else { + testRole = role; + } + cy.task('getSessionCookie', testRole).then((cookie) => { + cy.setCookie('sid', cookie as string); + }); + cy.visit('/'); + } else { + const user = role ? getEnvAuth(role) : defaultUser; + loginWithUser(user); + } }; export const loginWithUser = (user: User): void => { diff --git a/x-pack/test/security_solution_cypress/cypress/tsconfig.json b/x-pack/test/security_solution_cypress/cypress/tsconfig.json index 45b526793e98eb..d6a08efd3073f4 100644 --- a/x-pack/test/security_solution_cypress/cypress/tsconfig.json +++ b/x-pack/test/security_solution_cypress/cypress/tsconfig.json @@ -40,6 +40,6 @@ "@kbn/management-settings-ids", "@kbn/es-query", "@kbn/ml-plugin", - "@kbn/license-management-plugin" + "@kbn/license-management-plugin", ] }