Skip to content

Commit

Permalink
[Security Solution] Using API auth for API calls in Serverless Cypres…
Browse files Browse the repository at this point in the history
…s tests (elastic#190152)

### Context

In our Cypress tests, we use API calls to set up the data needed to run
the tests. Currently, we are using basic authentication for both ESS and
serverless environments. However, for serverless, we should be using API
key authentication, especially given that basic authentication will be
deprecated soon.

### Challenges

* Handling different authentication methods depending on whether the
environment is serverless or ESS.
* Allowing some tests to access or modify internal indexes.
* Managing how the username is handled across different tests.

### Implentation

To ensure the correct authentication is used based on the environment
where the tests are executed, the `rootRequest` method is used to build
the API request. Within this method, the appropriate authentication type
is selected.

All API calls will use an `admin` API key. The `admin` role is the least
restrictive, which is appropriate for setting up data for tests rather
than validating application behavior. This role minimizes the risk of
issues during setup.

A specific challenge arose when we needed to access or modify internal
indexes, a capability restricted to the `system_indices_superuser` role
for testing purposes. The issue stems from the API key generation
method, which is tied to the user's role rather than the user itself.
Since serverless currently lacks a role that permits access to internal
indexes, we are, upon recommendation from the appex-qa team, using the
Elasticsearch client directly with the `system_indices_superuser` role
for these scenarios.

For tests that assert the username, we made adjustments. Previously, the
`system_indices_superuser` role was used universally, which is no longer
the case for serverless. We now retrieve the username dynamically from
user information instead of hardcoding the value.

### To be discussed

When making modifications related to "username", it became apparent that
we sometimes use "fullname" and, in other cases, "username," even though
they seem intended to represent the same concept. Should we standardize
on a single term across the solution?

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
2 people authored and CAWilson94 committed Nov 18, 2024
1 parent 6cf619c commit f7a7d21
Show file tree
Hide file tree
Showing 18 changed files with 353 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { defineCypressConfig } from '@kbn/cypress-config';
import { esArchiver } from './support/es_archiver';
import { esClient } from './support/es_client';

export default defineCypressConfig({
chromeWebSecurity: false,
Expand All @@ -31,6 +32,7 @@ export default defineCypressConfig({
experimentalCspAllowList: ['default-src', 'script-src', 'script-src-elem'],
setupNodeEvents(on, config) {
esArchiver(on, config);
esClient(on, config);
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1920,1200');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { defineCypressConfig } from '@kbn/cypress-config';
import { esArchiver } from './support/es_archiver';
import { esClient } from './support/es_client';

// eslint-disable-next-line import/no-default-export
export default defineCypressConfig({
Expand Down Expand Up @@ -40,6 +41,7 @@ export default defineCypressConfig({
specPattern: './cypress/e2e/**/*.cy.ts',
setupNodeEvents(on, config) {
esArchiver(on, config);
esClient(on, config);
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1920,1200');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { defineCypressConfig } from '@kbn/cypress-config';
import { esArchiver } from './support/es_archiver';
import { samlAuthentication } from './support/saml_auth';
import { esClient } from './support/es_client';

// eslint-disable-next-line import/no-default-export
export default defineCypressConfig({
Expand Down Expand Up @@ -54,6 +55,8 @@ export default defineCypressConfig({
return launchOptions;
});
samlAuthentication(on, config);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
esClient(on, config);
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('@cypress/grep/src/plugin')(config);
return config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { defineCypressConfig } from '@kbn/cypress-config';
import { esArchiver } from './support/es_archiver';
import { samlAuthentication } from './support/saml_auth';
import { esClient } from './support/es_client';

// eslint-disable-next-line import/no-default-export
export default defineCypressConfig({
Expand All @@ -33,6 +34,7 @@ export default defineCypressConfig({
experimentalMemoryManagement: true,
setupNodeEvents(on, config) {
esArchiver(on, config);
esClient(on, config);
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--window-size=1920,1200');
Expand All @@ -46,6 +48,7 @@ export default defineCypressConfig({
return launchOptions;
});
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);
return config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getUsername } from '../../../../../../tasks/common';
import {
expectedExportedExceptionList,
getExceptionList,
Expand Down Expand Up @@ -97,11 +98,12 @@ describe(
exportExceptionList(getExceptionList1().list_id);

cy.wait('@export').then(({ response }) => {
cy.wrap(response?.body).should(
'eql',
expectedExportedExceptionList(exceptionListResponse)
);

getUsername('admin').then((username) => {
cy.wrap(response?.body).should(
'eql',
expectedExportedExceptionList(exceptionListResponse, username as string)
);
});
cy.get(TOASTER).should(
'have.text',
`Exception list "${EXCEPTION_LIST_NAME}" exported successfully`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,9 @@ import {
import { visit, visitWithTimeRange } from '../../../tasks/navigation';

import { CASES_URL, OVERVIEW_URL } from '../../../urls/navigation';
import { ELASTICSEARCH_USERNAME, IS_SERVERLESS } from '../../../env_var_names_constants';
import { deleteCases } from '../../../tasks/api_calls/cases';
import { login } from '../../../tasks/login';

const isServerless = Cypress.env(IS_SERVERLESS);
const getUsername = () => {
if (isServerless) {
return cy.task('getFullname');
} else {
return cy.wrap(Cypress.env(ELASTICSEARCH_USERNAME));
}
};
import { getFullname } from '../../../tasks/common';

// Tracked by https://github.com/elastic/security-team/issues/7696
describe('Cases', { tags: ['@ess', '@serverless'] }, () => {
Expand Down Expand Up @@ -120,7 +111,7 @@ describe('Cases', { tags: ['@ess', '@serverless'] }, () => {
`${this.mycase.description} ${this.mycase.timeline.title}`
);

getUsername().then((username) => {
getFullname('platform_engineer').then((username) => {
cy.get(CASE_DETAILS_USERNAMES).eq(REPORTER).should('contain', username);
cy.get(CASE_DETAILS_USERNAMES).eq(PARTICIPANTS).should('contain', username);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { expectedExportedTimelineTemplate } from '../../../objects/timeline';
import { TIMELINE_TEMPLATES_URL } from '../../../urls/navigation';
import { createTimelineTemplate, deleteTimelines } from '../../../tasks/api_calls/timelines';
import { searchByTitle } from '../../../tasks/table_pagination';
import { getFullname } from '../../../tasks/common';

describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
Expand All @@ -36,11 +37,12 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {

cy.wait('@export').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);

cy.wrap(response?.body).should(
'eql',
expectedExportedTimelineTemplate(this.templateResponse)
);
getFullname('admin').then((username) => {
cy.wrap(response?.body).should(
'eql',
expectedExportedTimelineTemplate(this.templateResponse, username as string)
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { TIMELINE_CHECKBOX } from '../../../screens/timelines';
import { createTimeline } from '../../../tasks/api_calls/timelines';
import { expectedExportedTimeline } from '../../../objects/timeline';
import { closeToast } from '../../../tasks/common/toast';
import { getFullname } from '../../../tasks/common';

describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
Expand Down Expand Up @@ -51,7 +52,12 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
exportTimeline(this.timelineId1);
cy.wait('@export').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body).should('eql', expectedExportedTimeline(this.timelineResponse1));
getFullname('admin').then((username) => {
cy.wrap(response?.body).should(
'eql',
expectedExportedTimeline(this.timelineResponse1, username as string)
);
});
});
closeToast();

Expand All @@ -61,7 +67,13 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
exportSelectedTimelines();
cy.wait('@export').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body).should('eql', expectedExportedTimeline(this.timelineResponse1));

getFullname('admin').then((username) => {
cy.wrap(response?.body).should(
'eql',
expectedExportedTimeline(this.timelineResponse1, username as string)
);
});
});

closeToast();
Expand All @@ -81,8 +93,17 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
cy.wait('@export').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
const timelines = response?.body?.split('\n');
assert.deepEqual(JSON.parse(timelines[0]), expectedExportedTimeline(this.timelineResponse2));
assert.deepEqual(JSON.parse(timelines[1]), expectedExportedTimeline(this.timelineResponse1));

getFullname('admin').then((username) => {
assert.deepEqual(
JSON.parse(timelines[0]),
expectedExportedTimeline(this.timelineResponse2, username as string)
);
assert.deepEqual(
JSON.parse(timelines[1]),
expectedExportedTimeline(this.timelineResponse1, username as string)
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createTimeline, deleteTimelines } from '../../../tasks/api_calls/timeli
import { login } from '../../../tasks/login';
import { visitTimeline } from '../../../tasks/navigation';
import { addNotesToTimeline, goToNotesTab } from '../../../tasks/timeline';
import { getFullname } from '../../../tasks/common';

const author = Cypress.env('ELASTICSEARCH_USERNAME');
const link = 'https://www.elastic.co/';
Expand Down Expand Up @@ -66,8 +67,10 @@ describe('Timeline notes tab', { tags: ['@ess', '@serverless'] }, () => {
});

it('should render the right author', () => {
addNotesToTimeline(getTimelineNonValidQuery().notes);
cy.get(NOTES_AUTHOR).first().should('have.text', author);
getFullname('admin').then((username) => {
addNotesToTimeline(getTimelineNonValidQuery().notes);
cy.get(NOTES_AUTHOR).first().should('have.text', username);
});
});

// this test is failing on MKI only, the change was introduced by this EUI PR https://github.com/elastic/kibana/pull/195525
Expand Down
16 changes: 3 additions & 13 deletions x-pack/test/security_solution_cypress/cypress/objects/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { ELASTICSEARCH_USERNAME } from '../env_var_names_constants';

export interface Exception {
field: string;
Expand Down Expand Up @@ -59,18 +58,9 @@ export const getException = (): Exception => ({
});

export const expectedExportedExceptionList = (
exceptionListResponse: Cypress.Response<ExceptionListSchema>
exceptionListResponse: Cypress.Response<ExceptionListSchema>,
username: string
): string => {
const jsonRule = exceptionListResponse.body;
return `{"_version":"${jsonRule._version}","created_at":"${
jsonRule.created_at
}","created_by":"${Cypress.env(ELASTICSEARCH_USERNAME)}","description":"${
jsonRule.description
}","id":"${jsonRule.id}","immutable":false,"list_id":"${jsonRule.list_id}","name":"${
jsonRule.name
}","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${
jsonRule.tie_breaker_id
}","type":"${jsonRule.type}","updated_at":"${jsonRule.updated_at}","updated_by":"${Cypress.env(
ELASTICSEARCH_USERNAME
)}","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`;
return `{"_version":"${jsonRule._version}","created_at":"${jsonRule.created_at}","created_by":"${username}","description":"${jsonRule.description}","id":"${jsonRule.id}","immutable":false,"list_id":"${jsonRule.list_id}","name":"${jsonRule.name}","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonRule.tie_breaker_id}","type":"${jsonRule.type}","updated_at":"${jsonRule.updated_at}","updated_by":"${username}","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export const getTimelineNonValidQuery = (): CompleteTimeline => ({
});

export const expectedExportedTimelineTemplate = (
templateResponse: Cypress.Response<PersistTimelineResponse>
templateResponse: Cypress.Response<PersistTimelineResponse>,
username: string
) => {
const timelineTemplateBody = templateResponse.body.data.persistTimeline.timeline;

Expand Down Expand Up @@ -102,9 +103,9 @@ export const expectedExportedTimelineTemplate = (
templateTimelineVersion: 1,
timelineType: 'template',
created: timelineTemplateBody.created,
createdBy: Cypress.env('ELASTICSEARCH_USERNAME'),
createdBy: username,
updated: timelineTemplateBody.updated,
updatedBy: Cypress.env('ELASTICSEARCH_USERNAME'),
updatedBy: username,
sort: [],
eventNotes: [],
globalNotes: [],
Expand All @@ -114,7 +115,8 @@ export const expectedExportedTimelineTemplate = (
};

export const expectedExportedTimeline = (
timelineResponse: Cypress.Response<PersistTimelineResponse>
timelineResponse: Cypress.Response<PersistTimelineResponse>,
username: string
) => {
const timelineBody = timelineResponse.body.data.persistTimeline.timeline;

Expand All @@ -140,9 +142,9 @@ export const expectedExportedTimeline = (
description: timelineBody.description,
title: timelineBody.title,
created: timelineBody.created,
createdBy: Cypress.env('ELASTICSEARCH_USERNAME'),
createdBy: username,
updated: timelineBody.updated,
updatedBy: Cypress.env('ELASTICSEARCH_USERNAME'),
updatedBy: username,
timelineType: 'default',
sort: [],
eventNotes: [],
Expand Down
Loading

0 comments on commit f7a7d21

Please sign in to comment.