diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index ef8d4097f21c1..c1ae7bb5f2b75 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -17,7 +17,7 @@ import { registry } from './registry'; interface Config { name: APMFtrConfigName; license: 'basic' | 'trial'; - kibanaConfig?: Record; + kibanaConfig?: Record; } const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async ( @@ -81,7 +81,9 @@ export function createTestConfig(config: Config) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), ...(kibanaConfig - ? Object.entries(kibanaConfig).map(([key, value]) => `--${key}=${value}`) + ? Object.entries(kibanaConfig).map(([key, value]) => + Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}` + ) : []), ], }, diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 5400c3af64b55..6bc4260de77be 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -42,7 +42,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const BULK_INDEX_DELAY = 1000; const INDEXING_DELAY = 5000; - const ALERTS_INDEX_TARGET = '.alerts-observability.apm.alerts*'; + const getAlertsTargetIndicesUrl = + '/api/observability/rules/alerts/dynamic_index_pattern?namespace=default®istrationContexts=observability.apm®istrationContexts='; + + const getAlertsTargetIndices = async () => + supertest.get(getAlertsTargetIndicesUrl).send().set('kbn-xsrf', 'foo'); const APM_METRIC_INDEX_NAME = 'apm-8.0.0-transaction'; const createTransactionMetric = (override: Record) => { @@ -92,6 +96,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { .get(`/api/alerts/alert/${alert.id}`) .set('kbn-xsrf', 'foo'); + const { body: targetIndices, status: targetIndicesStatus } = await getAlertsTargetIndices(); + if (targetIndices.length === 0) { + const error = new Error('Error getting alert'); + Object.assign(error, { response: { body: targetIndices, status: targetIndicesStatus } }); + throw error; + } + if (status >= 300) { const error = new Error('Error getting alert'); Object.assign(error, { response: { body, status } }); @@ -104,10 +115,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { await new Promise((resolve) => { setTimeout(resolve, BULK_INDEX_DELAY); }); - await es.indices.refresh({ - index: ALERTS_INDEX_TARGET, - }); + /** + * When calling refresh on an index pattern .alerts-observability.apm.alerts* (as was originally the hard-coded string in this test) + * The response from Elasticsearch is a 200, even if no indices which match that index pattern have been created. + * When calling refresh on a concrete index alias .alerts-observability.apm.alerts-default for instance, + * we receive a 404 error index_not_found_exception when no indices have been created which match that alias (obviously). + * Since we are receiving a concrete index alias from the observability api instead of a kibana index pattern + * and we understand / expect that this index does not exist at certain points of the test, we can try-catch at certain points without caring if the call fails. + * There are points in the code where we do want to ensure we get the appropriate error message back + */ + try { + await es.indices.refresh({ + index: targetIndices[0], + }); + // eslint-disable-next-line no-empty + } catch (exc) {} return nextAlert; } @@ -120,20 +143,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Rule registry with write enabled', { config: 'rules', archives: [] }, () => { it('does not bootstrap indices on plugin startup', async () => { - const { body } = await es.indices.get({ - index: ALERTS_INDEX_TARGET, - expand_wildcards: 'open', - allow_no_indices: true, - }); - - const indices = Object.entries(body).map(([indexName, index]) => { - return { - indexName, - index, - }; - }); - - expect(indices.length).to.be(0); + const { body: targetIndices } = await getAlertsTargetIndices(); + try { + const res = await es.indices.get({ + index: targetIndices[0], + expand_wildcards: 'open', + allow_no_indices: true, + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.statusCode).to.eql(404); + } }); describe('when creating a rule', () => { @@ -232,6 +252,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(async () => { + const { body: targetIndices } = await getAlertsTargetIndices(); if (createResponse.alert) { const { body, status } = await supertest .delete(`/api/alerts/alert/${createResponse.alert.id}`) @@ -245,7 +266,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } await es.deleteByQuery({ - index: ALERTS_INDEX_TARGET, + index: targetIndices[0], body: { query: { match_all: {}, @@ -263,25 +284,29 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(createResponse.status).to.be.below(299); expect(createResponse.alert).not.to.be(undefined); - let alert = await waitUntilNextExecution(createResponse.alert); - const beforeDataResponse = await es.search({ - index: ALERTS_INDEX_TARGET, - body: { - query: { - term: { - [EVENT_KIND]: 'signal', + const { body: targetIndices } = await getAlertsTargetIndices(); + + try { + const res = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', }, }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - }, - }); - - expect(beforeDataResponse.body.hits.hits.length).to.be(0); + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.message).contain('index_not_found_exception'); + } await es.index({ index: APM_METRIC_INDEX_NAME, @@ -295,22 +320,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { alert = await waitUntilNextExecution(alert); - const afterInitialDataResponse = await es.search({ - index: ALERTS_INDEX_TARGET, - body: { - query: { - term: { - [EVENT_KIND]: 'signal', + try { + const res = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', }, }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - }, - }); - - expect(afterInitialDataResponse.body.hits.hits.length).to.be(0); + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.message).contain('index_not_found_exception'); + } await es.index({ index: APM_METRIC_INDEX_NAME, @@ -325,7 +353,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { alert = await waitUntilNextExecution(alert); const afterViolatingDataResponse = await es.search({ - index: ALERTS_INDEX_TARGET, + index: targetIndices[0], body: { query: { term: { @@ -437,7 +465,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { alert = await waitUntilNextExecution(alert); const afterRecoveryResponse = await es.search({ - index: ALERTS_INDEX_TARGET, + index: targetIndices[0], body: { query: { term: { @@ -530,9 +558,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Rule registry with write not enabled', { config: 'basic', archives: [] }, () => { it('does not bootstrap the apm rule indices', async () => { + const { body: targetIndices } = await getAlertsTargetIndices(); const errorOrUndefined = await es.indices .get({ - index: ALERTS_INDEX_TARGET, + index: targetIndices[0], expand_wildcards: 'open', allow_no_indices: false, }) diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts new file mode 100644 index 0000000000000..938d74e7a4b08 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts @@ -0,0 +1,87 @@ +/* + * 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 { superUser, obsOnlySpacesAll, secOnlyRead } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User, space: string, expected: number = 200) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(expected); + return indexNames; + }; + + const getSecuritySolutionIndexName = async ( + user: User, + space: string, + expectedStatusCode: number = 200 + ) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(expectedStatusCode); + return indexNames; + }; + + describe('Alert - Get Index - RBAC - spaces', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + describe('Users:', () => { + it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }); + + it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const indexNames = await getAPMIndexName(superUser, SPACE1); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }); + + it(`${secOnlyRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { + const indexNames = await getAPMIndexName(secOnlyRead, SPACE1); + expect(indexNames?.index_name?.length).to.eql(0); + }); + + it(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => { + const indexNames = await getSecuritySolutionIndexName(secOnlyRead, SPACE1); + const securitySolution = indexNames?.index_name?.find((indexName) => + indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) + ); + expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index 7069aae292267..756a2ffb4a598 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -27,5 +27,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./update_alert')); loadTestFile(require.resolve('./bulk_update_alerts')); loadTestFile(require.resolve('./find_alerts')); + loadTestFile(require.resolve('./get_alerts_index')); }); };