From 8d83a075f6228bb5a6e35a9bb4654fe29cee0cff Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 6 Oct 2024 14:46:44 +0300 Subject: [PATCH] [ResponseOps][Alerting] Register anomaly detection and custom threshold rule types under stack alerts feature privilege (#194615) ## Summary In the ES query, anomaly detection, and custom threshold rule types users can use the "Role visibility" dropdown to select where the rules should be accessible. The "Role visibility" dropdown sets the `consumer` which is paramount for alerting RBAC. For the anomaly detection and custom threshold rule types if the `consumer` is set to `stackAlerts` then the rules will not be accessible from any rule page even if the user has access to the "Stack alerts" feature privilege. This PR fixes this bug. Fixes https://github.com/elastic/kibana/issues/193549 Fixes https://github.com/elastic/kibana/issues/191075 Fixes https://github.com/elastic/kibana/issues/184422 Fixes https://github.com/elastic/kibana/issues/179082 ## Testing 1. Create an anomaly detection and custom threshold rule and set the "Role visibility" to "Stack alerts". 2. Create a user with access only to "Stack alerts". 3. Login with the user created in Step 2. 4. Verify that you can see the rules from the stack management page. 5. Verify that you can see the alerts generated from the rules. 6. Create a user with roles `kibana_admin` and verify the same. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ## Release notes Fix bug where rule types with "Stack alerts" role visibility are not being shown in the stack management page --- .../stack_alerts/server/feature.test.ts | 51 +++++--- x-pack/plugins/stack_alerts/server/feature.ts | 51 +++++++- .../group1/tests/alerting/find.ts | 122 +++++++++++++++++- .../security_and_spaces/scenarios.ts | 43 ++++-- 4 files changed, 232 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/feature.test.ts b/x-pack/plugins/stack_alerts/server/feature.test.ts index 8935a8a43c5d2..769fad5172d65 100644 --- a/x-pack/plugins/stack_alerts/server/feature.test.ts +++ b/x-pack/plugins/stack_alerts/server/feature.test.ts @@ -27,26 +27,37 @@ describe('Stack Alerts Feature Privileges', () => { const featuresSetup = featuresPluginMock.createSetup(); plugin.setup(coreSetup, { alerting: alertingSetup, features: featuresSetup }); - const typesInFeaturePrivilege = BUILT_IN_ALERTS_FEATURE.alerting ?? []; - const typesInFeaturePrivilegeAll = - BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all ?? []; - const typesInFeaturePrivilegeRead = - BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? []; - // transform alerting rule is initialized during the transform plugin setup - expect(alertingSetup.registerType.mock.calls.length).toEqual( - typesInFeaturePrivilege.length - 1 - ); - expect(alertingSetup.registerType.mock.calls.length).toEqual( - typesInFeaturePrivilegeAll.length - 1 - ); - expect(alertingSetup.registerType.mock.calls.length).toEqual( - typesInFeaturePrivilegeRead.length - 1 - ); + expect(BUILT_IN_ALERTS_FEATURE.alerting).toMatchInlineSnapshot(` + Array [ + ".index-threshold", + ".geo-containment", + ".es-query", + "transform_health", + "observability.rules.custom_threshold", + "xpack.ml.anomaly_detection_alert", + ] + `); - alertingSetup.registerType.mock.calls.forEach((call) => { - expect(typesInFeaturePrivilege.indexOf(call[0].id)).toBeGreaterThanOrEqual(0); - expect(typesInFeaturePrivilegeAll.indexOf(call[0].id)).toBeGreaterThanOrEqual(0); - expect(typesInFeaturePrivilegeRead.indexOf(call[0].id)).toBeGreaterThanOrEqual(0); - }); + expect(BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all).toMatchInlineSnapshot(` + Array [ + ".index-threshold", + ".geo-containment", + ".es-query", + "transform_health", + "observability.rules.custom_threshold", + "xpack.ml.anomaly_detection_alert", + ] + `); + + expect(BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read).toMatchInlineSnapshot(` + Array [ + ".index-threshold", + ".geo-containment", + ".es-query", + "transform_health", + "observability.rules.custom_threshold", + "xpack.ml.anomaly_detection_alert", + ] + `); }); }); diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 8f3c809829c49..7130560b3232b 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -9,7 +9,11 @@ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { TRANSFORM_RULE_TYPE } from '@kbn/transform-plugin/common'; -import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; +import { + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + STACK_ALERTS_FEATURE_ID, +} from '@kbn/rule-data-utils'; import { ES_QUERY_ID as ElasticsearchQuery } from '@kbn/rule-data-utils'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ID as IndexThreshold } from './rule_types/index_threshold/rule_type'; @@ -28,7 +32,14 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], + alerting: [ + IndexThreshold, + GeoContainment, + ElasticsearchQuery, + TransformHealth, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + ], privileges: { all: { app: [], @@ -38,10 +49,24 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { }, alerting: { rule: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], + all: [ + IndexThreshold, + GeoContainment, + ElasticsearchQuery, + TransformHealth, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + ], }, alert: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], + all: [ + IndexThreshold, + GeoContainment, + ElasticsearchQuery, + TransformHealth, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + ], }, }, savedObject: { @@ -59,10 +84,24 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { }, alerting: { rule: { - read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], + read: [ + IndexThreshold, + GeoContainment, + ElasticsearchQuery, + TransformHealth, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + ], }, alert: { - read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], + read: [ + IndexThreshold, + GeoContainment, + ElasticsearchQuery, + TransformHealth, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + ], }, }, savedObject: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 8c11b69db03be..37d42ceeccb3a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -10,7 +10,12 @@ import { Agent as SuperTestAgent } from 'supertest'; import { chunk, omit } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; -import { SuperuserAtSpace1, UserAtSpaceScenarios } from '../../../scenarios'; +import { + ES_QUERY_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; +import { SuperuserAtSpace1, UserAtSpaceScenarios, StackAlertsOnly } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -663,5 +668,120 @@ export default function createFindTests({ getService }: FtrProviderContext) { findTestUtils('public', objectRemover, supertest, supertestWithoutAuth); findTestUtils('internal', objectRemover, supertest, supertestWithoutAuth); + + describe('stack alerts', () => { + const ruleTypes = [ + [ + ES_QUERY_ID, + { + searchType: 'esQuery', + timeWindowSize: 5, + timeWindowUnit: 'm', + threshold: [1000], + thresholdComparator: '>', + size: 100, + esQuery: '{\n "query":{\n "match_all" : {}\n }\n }', + aggType: 'count', + groupBy: 'all', + termSize: 5, + excludeHitsFromPreviousRun: false, + sourceFields: [], + index: ['.kibana'], + timeField: 'created_at', + }, + ], + [ + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + { + criteria: [ + { + comparator: '>', + metrics: [ + { + name: 'A', + aggType: 'count', + }, + ], + threshold: [100], + timeSize: 1, + timeUnit: 'm', + }, + ], + alertOnNoData: false, + alertOnGroupDisappear: false, + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: 'kibana-event-log-data-view', + }, + }, + ], + [ + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + { + severity: 75, + resultType: 'bucket', + includeInterim: false, + jobSelection: { + jobIds: ['low_request_rate'], + }, + }, + ], + ]; + + const createRule = async (rule = {}) => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix('space1')}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData({ ...rule })) + .expect(200); + + objectRemover.add('space1', createdAlert.id, 'rule', 'alerting'); + }; + + for (const [ruleTypeId, params] of ruleTypes) { + it(`should get rules of ${ruleTypeId} rule type ID and stackAlerts consumer`, async () => { + /** + * We create two rules. The first one is a test.noop + * rule with stackAlerts as consumer. The second rule + * is has different rule type ID but with the same consumer as the first rule (stackAlerts). + * This way we can verify that the find API call returns only the rules the user is authorized to. + * Specifically only the second rule because the StackAlertsOnly user does not have + * access to the test.noop rule type. + */ + await createRule({ consumer: 'stackAlerts' }); + await createRule({ rule_type_id: ruleTypeId, params, consumer: 'stackAlerts' }); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('space1')}/api/alerting/rules/_find`) + .auth(StackAlertsOnly.username, StackAlertsOnly.password); + + expect(response.statusCode).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].rule_type_id).to.equal(ruleTypeId); + expect(response.body.data[0].consumer).to.equal('stackAlerts'); + }); + } + + for (const [ruleTypeId, params] of ruleTypes) { + it(`should NOT get rules of ${ruleTypeId} rule type ID and NOT stackAlerts consumer`, async () => { + /** + * We create two rules with logs as consumer. The user is authorized to + * access rules only with the stackAlerts consumers. + */ + await createRule({ consumer: 'logs' }); + await createRule({ rule_type_id: ruleTypeId, params, consumer: 'logs' }); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('space1')}/api/alerting/rules/_find`) + .auth(StackAlertsOnly.username, StackAlertsOnly.password); + + expect(response.statusCode).to.eql(200); + expect(response.body.total).to.equal(0); + }); + } + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index a852657e0b891..fdb56a4fb501e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -190,6 +190,31 @@ const CasesAll: User = { }, }; +export const StackAlertsOnly: User = { + username: 'stack_alerts_only', + fullName: 'stack_alerts_only', + password: 'stack_alerts_only-password', + role: { + name: 'stack_alerts_only_role', + kibana: [ + { + feature: { + stackAlerts: ['all'], + }, + spaces: ['space1'], + }, + ], + elasticsearch: { + indices: [ + { + names: [`${ES_TEST_INDEX_NAME}*`], + privileges: ['all'], + }, + ], + }, + }, +}; + export const Users: User[] = [ NoKibanaPrivileges, Superuser, @@ -198,6 +223,7 @@ export const Users: User[] = [ Space1AllWithRestrictedFixture, Space1AllAlertingNoneActions, CasesAll, + StackAlertsOnly, ]; const Space1: Space = { @@ -256,14 +282,6 @@ const GlobalReadAtSpace1: GlobalReadAtSpace1 = { space: Space1, }; -interface Space1AllAtSpace1 extends Scenario { - id: 'space_1_all at space1'; -} -const Space1AllAtSpace1: Space1AllAtSpace1 = { - id: 'space_1_all at space1', - user: Space1All, - space: Space1, -}; interface Space1AllWithRestrictedFixtureAtSpace1 extends Scenario { id: 'space_1_all_with_restricted_fixture at space1'; } @@ -301,6 +319,15 @@ export const systemActionScenario: SystemActionSpace1 = { space: Space1, }; +interface Space1AllAtSpace1 extends Scenario { + id: 'space_1_all at space1'; +} +const Space1AllAtSpace1: Space1AllAtSpace1 = { + id: 'space_1_all at space1', + user: Space1All, + space: Space1, +}; + export const UserAtSpaceScenarios: [ NoKibanaPrivilegesAtSpace1, SuperuserAtSpace1,