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,