From 7523f7ee3440d64d641508b1d61c617e31ec41a5 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 8 Jun 2021 16:23:41 -0400 Subject: [PATCH] [RAC] [RBAC] adds function to get alerts-as-data index name (#6) * WIP - test script and route in rule registry to pull index name. I need to test out adding this route within the APM and sec sol plugins specifically and see if they spit back the same .alerts index but with the appropriate asset name despite not providing one. WIP - DO NOT DELETE THIS CODE minor cleanup updates client to require passing in index name, which is now available through the alerts as data client function getAlertsIndex fix types * remove outdated comment --- .../alerting_authorization.mock.ts | 1 + .../authorization/alerting_authorization.ts | 124 +++++++++++++----- .../apm/server/routes/settings/apm_indices.ts | 23 ++++ x-pack/plugins/apm/server/routes/typings.ts | 2 + .../alert_data_client/alerts_client.mock.ts | 1 + .../server/alert_data_client/alerts_client.ts | 41 +++--- .../alert_data_client/tests/get.test.ts | 8 +- .../alert_data_client/tests/update.test.ts | 10 +- x-pack/plugins/rule_registry/server/plugin.ts | 2 +- .../server/routes/get_alert_by_id.ts | 6 +- .../server/routes/get_alert_index.ts | 56 ++++++++ .../rule_registry/server/routes/index.ts | 2 + .../server/routes/update_alert_by_id.ts | 6 +- .../server/rule_data_client/index.ts | 5 + .../server/rule_data_plugin_service/index.ts | 16 ++- .../rule_data_plugin_service.mock.ts | 1 + .../server/scripts/get_alerts_index.sh | 24 ++++ .../tests/basic/get_alerts.ts | 45 ++++--- .../tests/basic/update_alert.ts | 27 +++- 19 files changed, 308 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/routes/get_alert_index.ts create mode 100755 x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 283f1f1b46ff9..09e7787d19fa9 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -16,6 +16,7 @@ const createAlertingAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), + getAuthorizedAlertsIndices: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 7506accd8b88e..6bc1eeff78d6b 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -81,6 +81,8 @@ export class AlertingAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly exemptConsumerIds: string[]; + // to be used when building the alerts as data index name + // private readonly spaceId: Promise; constructor({ alertTypeRegistry, @@ -124,6 +126,8 @@ export class AlertingAuthorization { return new Set(); }); + // this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id ?? undefined); + this.allPossibleConsumers = this.featuresIds.then((featuresIds) => featuresIds.size ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { @@ -138,6 +142,41 @@ export class AlertingAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { + const augmentedRuleTypes = await this.augmentRuleTypesWithAuthorization( + this.alertTypeRegistry.list(), + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert, + new Set(featureIds) + ); + + const arrayOfAuthorizedRuleTypes = Array.from(augmentedRuleTypes.authorizedRuleTypes); + + // As long as the user can read a minimum of one type of rule type produced by the provided feature, + // the user should be provided that features' alerts index. + // Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter + const authorizedFeatures = arrayOfAuthorizedRuleTypes.reduce( + (acc, ruleType) => acc.add(ruleType.producer), + new Set() + ); + + // when we add the spaceId to the index name, uncomment this line + // const spaceName = await this.spaceName; + + const toReturn = Array.from(authorizedFeatures).flatMap((feature) => { + switch (feature) { + case 'apm': + return '.alerts-observability-apm'; + case 'siem': + return ['.alerts-security-solution', '.siem-signals']; + default: + return []; + } + }); + + return toReturn; + } + public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; @@ -339,13 +378,15 @@ export class AlertingAuthorization { private async augmentRuleTypesWithAuthorization( ruleTypes: Set, operations: Array, - authorizationEntity: AlertingAuthorizationEntity + authorizationEntity: AlertingAuthorizationEntity, + featuresIds?: Set ): Promise<{ username?: string; hasAllRequested: boolean; authorizedRuleTypes: Set; + unauthorizedRuleTypes: Set | undefined; }> { - const featuresIds = await this.featuresIds; + const fIds = featuresIds ?? (await this.featuresIds); if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -363,7 +404,7 @@ export class AlertingAuthorization { // as we can't ask ES for the user's individual privileges we need to ask for each feature // and ruleType in the system whether this user has this privilege for (const ruleType of ruleTypesWithAuthorization) { - for (const feature of featuresIds) { + for (const feature of fIds) { for (const operation of operations) { privilegeToRuleType.set( this.authorization!.actions.alerting.get( @@ -382,47 +423,62 @@ export class AlertingAuthorization { kibana: [...privilegeToRuleType.keys()], }); + let authorizedRuleTypes; + let unauthorizedRuleTypes; + if (hasAllRequested) { + authorizedRuleTypes = this.augmentWithAuthorizedConsumers( + ruleTypes, + await this.allPossibleConsumers + ); + } else { + [authorizedRuleTypes, unauthorizedRuleTypes] = privileges.kibana.reduce( + ([authzRuleTypes, unauthzRuleTypes], { authorized, privilege }) => { + if (authorized && privilegeToRuleType.has(privilege)) { + const [ + ruleType, + feature, + hasPrivileges, + isAuthorizedAtProducerLevel, + ] = privilegeToRuleType.get(privilege)!; + ruleType.authorizedConsumers[feature] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[feature] + ); + + if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) { + // granting privileges under the producer automatically authorized exempt consumer IDs as well + this.exemptConsumerIds.forEach((exemptId: string) => { + ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[exemptId] + ); + }); + } + authzRuleTypes.add(ruleType); + } else if (!authorized) { + const [ruleType, , , ,] = privilegeToRuleType.get(privilege)!; + unauthzRuleTypes.add(ruleType); + } + return [authzRuleTypes, unauthzRuleTypes]; + }, + [new Set(), new Set()] + ); + } + return { username, hasAllRequested, - authorizedRuleTypes: hasAllRequested - ? // has access to all features - this.augmentWithAuthorizedConsumers(ruleTypes, await this.allPossibleConsumers) - : // only has some of the required privileges - privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => { - if (authorized && privilegeToRuleType.has(privilege)) { - const [ - ruleType, - feature, - hasPrivileges, - isAuthorizedAtProducerLevel, - ] = privilegeToRuleType.get(privilege)!; - ruleType.authorizedConsumers[feature] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[feature] - ); - - if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) { - // granting privileges under the producer automatically authorized exempt consumer IDs as well - this.exemptConsumerIds.forEach((exemptId: string) => { - ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[exemptId] - ); - }); - } - authorizedRuleTypes.add(ruleType); - } - return authorizedRuleTypes; - }, new Set()), + authorizedRuleTypes, + unauthorizedRuleTypes, }; } else { return { hasAllRequested: true, authorizedRuleTypes: this.augmentWithAuthorizedConsumers( - new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))), + new Set([...ruleTypes].filter((ruleType) => fIds.has(ruleType.producer))), await this.allPossibleConsumers ), + unauthorizedRuleTypes: undefined, }; } } diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 003471aa89f39..7eaebc0eae67c 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -13,6 +13,7 @@ import { getApmIndexSettings, } from '../../lib/settings/apm_indices/get_apm_indices'; import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; +// import { APM_SERVER_FEATURE_ID } from '../../../common/alert_types'; // get list of apm indices and values const apmIndexSettingsRoute = createApmServerRoute({ @@ -63,7 +64,29 @@ const saveApmIndicesRoute = createApmServerRoute({ }, }); +// const getApmAlertsAsDataIndexRoute = createApmServerRoute({ +// endpoint: 'GET /api/apm/settings/apm-alerts-as-data-indices', +// options: { tags: ['access:apm'] }, +// handler: async (resources) => { +// const { context } = resources; +// console.error(context); +// const alertsAsDataClient = await context.rac?.getAlertsClient(); +// if (alertsAsDataClient == null) { +// throw new Error('Missing alerts as data client'); +// } +// const res = await alertsAsDataClient.getAlertsIndex([ +// APM_SERVER_FEATURE_ID, +// 'siem', +// ]); +// console.error('RESPONSE', JSON.stringify(res, null, 2)); +// return res[0]; +// // return alertsAsDataClient.getFullAssetName(); +// // return ruleDataClient.getIndexName(); +// }, +// }); + export const apmIndicesRouteRepository = createApmServerRouteRepository() .add(apmIndexSettingsRoute) .add(apmIndicesRoute) .add(saveApmIndicesRoute); +// .add(getApmAlertsAsDataIndexRoute); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 13bd631085aac..31fa6134c1091 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,6 +14,7 @@ import { } from 'src/core/server'; import { RuleDataClient } from '../../../rule_registry/server'; import { AlertingApiRequestHandlerContext } from '../../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../../rule_registry/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { APMConfig } from '..'; import { APMPluginDependencies } from '../types'; @@ -21,6 +22,7 @@ import { APMPluginDependencies } from '../types'; export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { licensing: LicensingApiRequestHandlerContext; alerting: AlertingApiRequestHandlerContext; + rac: RacApiRequestHandlerContext; } export type InspectResponse = Array<{ diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts index 26da30ce3c194..07ed470285a43 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -16,6 +16,7 @@ const createAlertsClientMock = () => { get: jest.fn(), getAlertsIndex: jest.fn(), update: jest.fn(), + getFullAssetName: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 617c369da1f9c..a88a5d759223b 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -17,6 +17,7 @@ import { Logger, ElasticsearchClient } from '../../../../../src/core/server'; import { alertAuditEvent, AlertAuditAction } from './audit_events'; import { RuleDataPluginService } from '../rule_data_plugin_service'; import { AuditLogger } from '../../../security/server'; +import { OWNER, RULE_ID } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; export interface ConstructorOptions { @@ -33,13 +34,13 @@ export interface UpdateOptions { status: string; }; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 - assetName: string; + indexName: string; } interface GetAlertParams { id: string; // observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191 - assetName: string; + indexName: string; } export class AlertsClient { @@ -63,27 +64,27 @@ export class AlertsClient { this.ruleDataService = ruleDataService; } - /** - * we are "hard coding" this string similar to how rule registry is doing it - * x-pack/plugins/apm/server/plugin.ts:191 - */ - public getAlertsIndex(assetName: string) { - return this.ruleDataService?.getFullAssetName(assetName); + public getFullAssetName() { + return this.ruleDataService?.getFullAssetName(); } - private async fetchAlert({ id, assetName }: GetAlertParams): Promise { + public async getAlertsIndex(featureIds: string[]) { + return this.authorization.getAuthorizedAlertsIndices( + featureIds.length !== 0 ? featureIds : ['apm', 'siem'] + ); + } + + private async fetchAlert({ id, indexName }: GetAlertParams): Promise { try { const result = await this.esClient.get({ - index: this.getAlertsIndex(assetName), + index: indexName, id, }); if ( - result == null || - result.body == null || result.body._source == null || - result.body._source['rule.id'] == null || - result.body._source['kibana.rac.alert.owner'] == null + result.body._source[RULE_ID] == null || + result.body._source[OWNER] == null ) { const errorMessage = `[rac] - Unable to retrieve alert details for alert with id of "${id}".`; this.logger.debug(errorMessage); @@ -98,12 +99,12 @@ export class AlertsClient { } } - public async get({ id, assetName }: GetAlertParams): Promise { + public async get({ id, indexName }: GetAlertParams): Promise { try { // first search for the alert by id, then use the alert info to check if user has access to it const alert = await this.fetchAlert({ id, - assetName, + indexName, }); // this.authorization leverages the alerting plugin's authorization @@ -139,13 +140,13 @@ export class AlertsClient { public async update({ id, data, - assetName, + indexName, }: UpdateOptions): Promise { try { // TODO: use MGET const alert = await this.fetchAlert({ id, - assetName, + indexName, }); await this.authorization.ensureAuthorized({ @@ -155,11 +156,9 @@ export class AlertsClient { entity: AlertingAuthorizationEntity.Alert, }); - const index = this.getAlertsIndex(assetName); - const updateParameters = { id, - index, + index: indexName, body: { doc: { 'kibana.rac.alert.status': data.status, diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 03796a57facab..f435c7399ab72 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -57,7 +57,7 @@ describe('get()', () => { }, }) ); - const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' }); + const result = await alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' }); expect(result).toMatchInlineSnapshot(` Object { "kibana.rac.alert.owner": "apm", @@ -88,7 +88,7 @@ describe('get()', () => { esClientMock.get.mockRejectedValue(error); await expect( - alertsClient.get({ id: '1', assetName: 'observability-apm' }) + alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong"`); expect(auditLogger.log).toHaveBeenCalledWith({ error: { code: 'Error', message: 'something when wrong' }, @@ -119,7 +119,7 @@ describe('get()', () => { test('returns alert if user is authorized to read alert under the consumer', async () => { const alertsClient = new AlertsClient(alertsClientParams); - const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' }); + const result = await alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' }); expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ entity: 'alert', @@ -144,7 +144,7 @@ describe('get()', () => { ); await expect( - alertsClient.get({ id: '1', assetName: 'observability-apm' }) + alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' }) ).rejects.toMatchInlineSnapshot( `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` ); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index e1edba023406d..1e1925c00c570 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -88,7 +88,7 @@ describe('update()', () => { const result = await alertsClient.update({ id: '1', data: { status: 'closed' }, - assetName: 'observability-apm', + indexName: '.alerts-observability-apm', }); expect(result).toMatchInlineSnapshot(` Object { @@ -133,7 +133,7 @@ describe('update()', () => { alertsClient.update({ id: '1', data: { status: 'closed' }, - assetName: 'observability-apm', + indexName: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong on get"`); expect(auditLogger.log).toHaveBeenCalledWith({ @@ -173,7 +173,7 @@ describe('update()', () => { alertsClient.update({ id: '1', data: { status: 'closed' }, - assetName: 'observability-apm', + indexName: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong on update"`); expect(auditLogger.log).toHaveBeenCalledWith({ @@ -242,7 +242,7 @@ describe('update()', () => { const result = await alertsClient.update({ id: '1', data: { status: 'closed' }, - assetName: 'observability-apm', + indexName: '.alerts-observability-apm', }); expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ @@ -271,7 +271,7 @@ describe('update()', () => { alertsClient.update({ id: '1', data: { status: 'closed' }, - assetName: 'observability-apm', + indexName: '.alerts-observability-apm', }) ).rejects.toMatchInlineSnapshot( `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 9e63b0a39230b..81f923390c9d9 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -117,7 +117,7 @@ export class RuleRegistryPlugin // handler is called when '/path' resource is requested with `GET` method router.get({ path: '/rac-myfakepath', validate: false }, async (context, req, res) => { const racClient = await context.rac.getAlertsClient(); - racClient?.get({ id: 'hello world', assetName: 'observability-apm' }); + racClient?.get({ id: 'hello world', indexName: '.alerts-observability-apm' }); return res.ok(); }); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts index 039c10d4c37a1..1c1f98e3c97fa 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts @@ -23,7 +23,7 @@ export const getAlertByIdRoute = (router: IRouter) => t.exact( t.type({ id: _id, - assetName: t.string, + indexName: t.string, }) ) ), @@ -35,8 +35,8 @@ export const getAlertByIdRoute = (router: IRouter) => async (context, request, response) => { try { const alertsClient = await context.rac.getAlertsClient(); - const { id, assetName } = request.query; - const alert = await alertsClient.get({ id, assetName }); + const { id, indexName } = request.query; + const alert = await alertsClient.get({ id, indexName }); return response.ok({ body: alert, }); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts new file mode 100644 index 0000000000000..03298e705acaa --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -0,0 +1,56 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; + +export const getAlertsIndexRoute = (router: IRouter) => { + router.get( + { + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + validate: false, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + const APM_SERVER_FEATURE_ID = 'apm'; + const SERVER_APP_ID = 'siem'; + try { + const alertsClient = await context.rac.getAlertsClient(); + const indexName = await alertsClient.getAlertsIndex([APM_SERVER_FEATURE_ID, SERVER_APP_ID]); + return response.ok({ + body: { index_name: indexName }, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.custom({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: Buffer.from( + JSON.stringify({ + message: err.message, + status_code: err.statusCode, + }) + ), + }); + // return response.custom; + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts index 4cc7881bf94e0..6698cd7717268 100644 --- a/x-pack/plugins/rule_registry/server/routes/index.ts +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -9,8 +9,10 @@ import { IRouter } from 'kibana/server'; import { RacRequestHandlerContext } from '../types'; import { getAlertByIdRoute } from './get_alert_by_id'; import { updateAlertByIdRoute } from './update_alert_by_id'; +import { getAlertsIndexRoute } from './get_alert_index'; export function defineRoutes(router: IRouter) { getAlertByIdRoute(router); updateAlertByIdRoute(router); + getAlertsIndexRoute(router); } diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts index 66f89d02d5a2e..837905149b7a7 100644 --- a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts @@ -21,7 +21,7 @@ export const updateAlertByIdRoute = (router: IRouter) body: schema.object({ status: schema.string(), ids: schema.arrayOf(schema.string()), - assetName: schema.string(), + indexName: schema.string(), }), }, options: { @@ -31,12 +31,12 @@ export const updateAlertByIdRoute = (router: IRouter) async (context, req, response) => { try { const racClient = await context.rac.getAlertsClient(); - const { status, ids, assetName } = req.body; + const { status, ids, indexName } = req.body; const thing = await racClient?.update({ id: ids[0], data: { status }, - assetName, + indexName, }); return response.ok({ body: { success: true, alerts: thing } }); } catch (exc) { diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts index cd7467c903e52..5ff527c87601e 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -26,6 +26,11 @@ export class RuleDataClient implements IRuleDataClient { return await this.options.getClusterClient(); } + getIndexName() { + // const index = `${[this.options.alias, options.namespace].filter(Boolean).join('-')}*`; + return this.options.alias; + } + getReader(options: { namespace?: string } = {}): RuleDataReader { const index = `${[this.options.alias, options.namespace].filter(Boolean).join('-')}*`; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index 68992ef83e081..d7502ff1c864b 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -16,6 +16,7 @@ import { import { ecsComponentTemplate } from '../../common/assets/component_templates/ecs_component_template'; import { defaultLifecyclePolicy } from '../../common/assets/lifecycle_policies/default_lifecycle_policy'; import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../common/types'; +// import { ClusterClient } from 'src/core/server/elasticsearch/client'; const BOOTSTRAP_TIMEOUT = 60000; @@ -50,8 +51,11 @@ function createSignal() { export class RuleDataPluginService { signal = createSignal(); + private readonly fullAssetName; - constructor(private readonly options: RuleDataPluginServiceConstructorOptions) {} + constructor(private readonly options: RuleDataPluginServiceConstructorOptions) { + this.fullAssetName = options.index; + } private assertWriteEnabled() { if (!this.isWriteEnabled) { @@ -153,6 +157,14 @@ export class RuleDataPluginService { } getFullAssetName(assetName?: string) { - return [this.options.index, assetName].filter(Boolean).join('-'); + // return [this.options.index, assetName].filter(Boolean).join('-'); + return [this.fullAssetName, assetName].filter(Boolean).join('-'); + } + + async assertFullAssetNameExists(assetName?: string) { + const fullAssetName = this.getFullAssetName(assetName); + const clusterClient = await this.getClusterClient(); + const { body } = await clusterClient.indices.exists({ index: fullAssetName }); + return body; } } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts index d5f89ad8b7889..7e1dec1750753 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -20,6 +20,7 @@ const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOpti createOrUpdateComponentTemplate: jest.fn(), createOrUpdateIndexTemplate: jest.fn(), createOrUpdateLifecyclePolicy: jest.fn(), + assertFullAssetNameExists: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh new file mode 100755 index 0000000000000..ed6c614a11bd7 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} +ASSET_NAME=${2:-'observability-apm'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/rac/alerts/index?assetName=siem" | jq . + +# -X GET "${KIBANA_URL}${SPACE_URL}/api/apm/settings/apm-alerts-as-data-indices" | jq . diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts index 49812a82adf2c..5314789f748f9 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts.ts @@ -5,6 +5,8 @@ * 2.0. */ +import expect from '@kbn/expect'; + import { secOnly, secOnlyRead, @@ -28,53 +30,63 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const TEST_URL = '/api/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; const SPACE2 = 'space2'; + const getAPMIndexName = async (user) => { + const { body: indexNames } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames.index_name.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + describe('rbac', () => { before(async () => { await esArchiver.load('rule_registry/alerts'); }); describe('Users:', () => { it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( - `${getSpaceUrlPrefix( - SPACE1 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + `${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(superUser.username, superUser.password) .set('kbn-xsrf', 'true') .expect(200); }); it(`${globalRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( - `${getSpaceUrlPrefix( - SPACE1 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + `${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(globalRead.username, globalRead.password) .set('kbn-xsrf', 'true') .expect(200); }); it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( - `${getSpaceUrlPrefix( - SPACE1 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + `${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(obsOnlySpacesAll.username, obsOnlySpacesAll.password) .set('kbn-xsrf', 'true') .expect(200); }); it(`${obsOnlyReadSpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( - `${getSpaceUrlPrefix( - SPACE1 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + `${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(obsOnlyReadSpacesAll.username, obsOnlyReadSpacesAll.password) .set('kbn-xsrf', 'true') @@ -93,11 +105,12 @@ export default ({ getService }: FtrProviderContext) => { }, ]) { it(`${scenario.user.username} should not be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( `${getSpaceUrlPrefix( SPACE1 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(scenario.user.username, scenario.user.password) .set('kbn-xsrf', 'true') @@ -112,11 +125,12 @@ export default ({ getService }: FtrProviderContext) => { { user: globalRead, space: SPACE1 }, ]) { it(`${scenario.user.username} should be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( `${getSpaceUrlPrefix( SPACE2 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(scenario.user.username, scenario.user.password) .set('kbn-xsrf', 'true') @@ -140,11 +154,12 @@ export default ({ getService }: FtrProviderContext) => { }, ]) { it(`${scenario.user.username} with right to access space1 only, should not be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .get( `${getSpaceUrlPrefix( SPACE2 - )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&assetName=observability-apm` + )}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&indexName=${apmIndex}` ) .auth(scenario.user.username, scenario.user.password) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts index a65b7b002d68e..d3598fe2342db 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import expect from '@kbn/expect'; import { secOnly, @@ -22,8 +23,22 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const TEST_URL = '/api/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; + const getAPMIndexName = async (user) => { + const { body: indexNames } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames.index_name.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + describe('rbac', () => { describe('Users update:', () => { beforeEach(async () => { @@ -33,11 +48,12 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.unload('rule_registry/alerts'); }); it(`${superUser.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) .auth(superUser.username, superUser.password) .set('kbn-xsrf', 'true') - .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', indexName: apmIndex }) .expect(200); }); // it(`${globalRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { @@ -49,19 +65,21 @@ export default ({ getService }: FtrProviderContext) => { // // console.error('RES', res); // }); it(`${obsOnlySpacesAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) .auth(obsOnlySpacesAll.username, obsOnlySpacesAll.password) .set('kbn-xsrf', 'true') - .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', indexName: apmIndex }) .expect(200); }); it(`${obsOnlyReadSpacesAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) .auth(obsOnlyReadSpacesAll.username, obsOnlyReadSpacesAll.password) .set('kbn-xsrf', 'true') - .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', assetName: 'observability-apm' }) + .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', indexName: apmIndex }) .expect(403); }); @@ -77,6 +95,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]) { it(`${scenario.user.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); await supertestWithoutAuth .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) .auth(scenario.user.username, scenario.user.password) @@ -84,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: ['NoxgpHkBqbdrfX07MqXV'], status: 'closed', - assetName: 'observability-apm', + indexName: apmIndex, }) .expect(403); });