From 47ea2868dfe686fb4b5d1ad024c29b069b3e8f0a Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:36:18 -0700 Subject: [PATCH 01/15] [Response Ops][RAM] Add RBAC Support for Multi-Consumer Rule Creation in O11Y and Stack Management (#162605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves: - https://github.com/elastic/kibana/issues/162484 - https://github.com/elastic/kibana/issues/160677 This PR adds the ability for `logs` and/or `infrastructure` only users to create and modify ES Query and new Generic Threshold rules. The `ensureAuthorized` function is modified and simplified to support this use case, by skipping producer authorization and only authorizing for consumers. When the consumer is `alerts`, we will consider this legacy and replace it with the rule’s producer (`consumer = ruleType.producer`) There is now a dropdown in the rule form to prompt the user when creating ES Query/Generic threshold rules to select the consumer based on their authorized consumers (we can no longer use `alerts` for these). If there is only 1 option, then the dropdown will not be shown and the option will be chosen automatically. Generic threshold rules will have the following possible consumers: - slo - infrastructure - logs - apm - uptime ES query rules will have the following possible consumers: - slo - infrastructure - logs - apm - uptime - stackAlerts ## To Test: ### Single Consumer: 1. Create a user with only `logs` feature enabled (ensuring `stackAlerts` is not enabled). 2. Navigate to the O11Y rule management page 3. Click the create rule button 4. Assert that both ES query and generic threshold rules are available 5. Click ES query and fill out the relevant information and create the rule 6. Assert that the rule created has `logs` set in the `consumer` field 7. Repeat 5-6 for the generic threshold rule 8. Repeat 2-7 but on the Stack Management rules page 9. Repeat 1-8 for the `infrastructure` feature. ### Multiple Consumers: 1. Create a user with `logs`, `infrastructure` and `apm` features enabled (ensuring `stackAlerts` is not enabled). 2. Navigate to the O11Y rule management page 3. Click the create rule button 4. Assert that both ES query and generic threshold rules are available 5. Click ES query and fill out the relevant information and create the rule 6. A dropdown should prompt the user to select between 1 of the 3 consumers, select 1 7. Assert that the rule was created with the selected consumer 8. Repeat 5-7 for the generic threshold rule 9. Repeat 2-8 but on the Stack Management rules page ![Screenshot from 2023-08-08 16-45-43](https://github.com/elastic/kibana/assets/74562234/8c5b644a-8bab-4c1b-93b0-acfa956af19c) ![consumer_dropdown_open](https://github.com/elastic/kibana/assets/74562234/a03b7e97-e90e-4bbc-bed0-94a6c677d31d) ### 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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../alerting_authorization.test.ts | 98 ++++--- .../authorization/alerting_authorization.ts | 96 ++----- x-pack/plugins/apm/server/feature.ts | 18 +- x-pack/plugins/apm/tsconfig.json | 3 +- x-pack/plugins/infra/server/features.ts | 35 ++- x-pack/plugins/infra/tsconfig.json | 3 +- .../plugins/observability/common/constants.ts | 11 + .../hooks/use_get_filtered_rule_types.ts | 5 +- .../public/pages/alerts/alerts.tsx | 7 +- .../pages/rule_details/rule_details.tsx | 6 +- .../public/pages/rules/rules.tsx | 2 + .../pages/slos/components/slo_list_item.tsx | 6 +- x-pack/plugins/observability/server/index.ts | 2 +- x-pack/plugins/observability/server/plugin.ts | 18 +- .../plugins/stack_alerts/common/constants.ts | 1 + x-pack/plugins/stack_alerts/common/index.ts | 2 +- x-pack/plugins/stack_alerts/kibana.jsonc | 3 +- .../server/rule_types/es_query/constants.ts | 2 +- x-pack/plugins/synthetics/server/feature.ts | 20 +- x-pack/plugins/synthetics/tsconfig.json | 1 + .../public/application/constants/index.ts | 9 +- .../public/application/lib/capabilities.ts | 12 + .../sections/rule_form/rule_add.test.tsx | 267 ++++++++++++++++-- .../sections/rule_form/rule_add.tsx | 14 +- .../sections/rule_form/rule_form.test.tsx | 243 ++++++++++++++-- .../sections/rule_form/rule_form.tsx | 146 ++++++++-- .../rule_form_consumer_selection.test.tsx | 109 +++++++ .../rule_form_consumer_selection.tsx | 142 ++++++++++ .../triggers_actions_ui/public/index.ts | 1 + .../triggers_actions_ui/public/types.ts | 10 + .../common/lib/alert_utils.ts | 16 +- .../common/lib/index.ts | 6 +- .../threshold_rule/avg_pct_fired.ts | 6 +- .../threshold_rule/avg_pct_no_data.ts | 6 +- .../threshold_rule/avg_us_fired.ts | 6 +- .../custom_eq_avg_bytes_fired.ts | 6 +- .../threshold_rule/documents_count_fired.ts | 6 +- .../threshold_rule/group_by_fired.ts | 6 +- .../observability/threshold_rule_data_view.ts | 2 +- .../group1/tests/alerting/create.ts | 52 +--- .../group1/tests/alerting/delete.ts | 41 +-- .../group1/tests/alerting/disable.ts | 38 +-- .../group1/tests/alerting/enable.ts | 44 +-- .../group1/tests/alerting/get.ts | 28 +- .../group1/tests/alerting/get_alert_state.ts | 18 +- .../tests/alerting/get_alert_summary.ts | 18 +- .../group2/tests/alerting/alerts.ts | 34 +-- .../group2/tests/alerting/mute_all.ts | 32 +-- .../group2/tests/alerting/mute_instance.ts | 38 +-- .../group2/tests/alerting/snooze.ts | 38 +-- .../group2/tests/alerting/unmute_all.ts | 32 +-- .../group2/tests/alerting/unmute_instance.ts | 28 +- .../group2/tests/alerting/unsnooze.ts | 32 +-- .../group2/tests/alerting/update.ts | 68 +---- .../group2/tests/alerting/update_api_key.ts | 34 +-- .../group3/tests/alerting/bulk_delete.ts | 24 +- .../group3/tests/alerting/bulk_disable.ts | 26 +- .../group3/tests/alerting/bulk_edit.ts | 24 +- .../group3/tests/alerting/bulk_enable.ts | 24 +- .../group3/tests/alerting/clone.ts | 8 +- .../group3/tests/alerting/run_soon.ts | 8 +- .../tests/alerting/group1/create.ts | 4 +- .../apis/maps/maps_telemetry.ts | 4 +- .../apps/observability/pages/rules_page.ts | 142 ++++++++-- .../check_registered_task_types.ts | 1 + .../threshold_rule/avg_pct_fired.ts | 6 +- .../threshold_rule/avg_pct_no_data.ts | 6 +- .../custom_eq_avg_bytes_fired.ts | 6 +- .../threshold_rule/documents_count_fired.ts | 6 +- .../threshold_rule/group_by_fired.ts | 6 +- 70 files changed, 1403 insertions(+), 819 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 333623588aa64..e2d07c2d4a359 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -249,7 +249,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }); - expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(0); + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); }); test('is a no-op when the security license is disabled', async () => { @@ -271,7 +271,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }); - expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(0); + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); }); test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when producer and consumer are the same', async () => { @@ -304,7 +304,7 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myApp', @@ -346,7 +346,7 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myApp', @@ -388,13 +388,7 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'alerts', - 'rule', - 'create' - ); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myApp', @@ -436,13 +430,7 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'alerts', - 'alert', - 'update' - ); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myApp', @@ -484,13 +472,7 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'rule', - 'create' - ); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myOtherApp', @@ -498,10 +480,7 @@ describe('AlertingAuthorization', () => { 'create' ); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [ - mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), - mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), - ], + kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create')], }); }); @@ -535,24 +514,57 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'myApp', + 'myOtherApp', 'alert', 'update' ); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update')], + }); + }); + + test('ensures the producer is used for authorization if the consumer is `alerts`', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { kibana: [] }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + getSpace, + getSpaceId, + }); + + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }); + + expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'myOtherApp', - 'alert', - 'update' + 'myApp', + 'rule', + 'create' ); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [ - mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), - mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), - ], + kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], }); }); @@ -596,7 +608,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" rule for \\"myOtherApp\\""` + `"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" rule"` ); }); @@ -644,7 +656,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to update a \\"myType\\" alert for \\"myAppRulesOnly\\""` + `"Unauthorized by \\"myAppRulesOnly\\" to update \\"myType\\" alert"` ); }); @@ -688,7 +700,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to update a \\"myType\\" alert by \\"myApp\\""` + `"Unauthorized by \\"myOtherApp\\" to update \\"myType\\" alert"` ); }); @@ -732,7 +744,7 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + `"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" alert"` ); }); }); @@ -950,7 +962,7 @@ describe('AlertingAuthorization', () => { expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert'); }).toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` + `"Unauthorized by \\"myOtherApp\\" to find \\"myAppAlertType\\" alert"` ); }); test('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => { diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 7264898c10648..cc9018e0387d1 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { map, mapValues, fromPairs, has } from 'lodash'; +import { fromPairs, has } from 'lodash'; import { KibanaRequest } from '@kbn/core/server'; import { JsonObject } from '@kbn/utility-types'; import { KueryNode } from '@kbn/es-query'; @@ -169,42 +169,24 @@ export class AlertingAuthorization { ); } - public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { + public async ensureAuthorized({ + ruleTypeId, + consumer: legacyConsumer, + operation, + entity, + }: EnsureAuthorizedOpts) { const { authorization } = this; + const ruleType = this.ruleTypeRegistry.get(ruleTypeId); + // We have some rules with consumer of "alerts" which indirectly means the same as + // a consumer of the rule type producer. Let's simplify the code and set it accordingly + const consumer = legacyConsumer === ALERTS_FEATURE_ID ? ruleType.producer : legacyConsumer; const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { - const ruleType = this.ruleTypeRegistry.get(ruleTypeId); - const requiredPrivilegesByScope = { - consumer: authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation), - producer: authorization.actions.alerting.get( - ruleTypeId, - ruleType.producer, - entity, - operation - ), - }; - - // Skip authorizing consumer if consumer is the Rules Management consumer (`alerts`) - // This means that rules and their derivative alerts created in the Rules Management UI - // will only be subject to checking if user has access to the rule producer. - const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; - const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, privileges } = await checkPrivileges({ - kibana: - shouldAuthorizeConsumer && consumer !== ruleType.producer - ? [ - // check for access at consumer level - requiredPrivilegesByScope.consumer, - // check for access at producer level - requiredPrivilegesByScope.producer, - ] - : [ - // skip consumer privilege checks under `alerts` as all rule types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ], + + const { hasAllRequested } = await checkPrivileges({ + kibana: [authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation)], }); if (!isAvailableConsumer) { @@ -215,40 +197,14 @@ export class AlertingAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden( - getUnauthorizedMessage(ruleTypeId, ScopeType.Consumer, consumer, operation, entity) - ); + throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, legacyConsumer, operation, entity)); } if (!hasAllRequested) { - const authorizedPrivileges = map( - privileges.kibana.filter((privilege) => privilege.authorized), - 'privilege' - ); - const unauthorizedScopes = mapValues( - requiredPrivilegesByScope, - (privilege) => !authorizedPrivileges.includes(privilege) - ); - - const [unauthorizedScopeType, unauthorizedScope] = - shouldAuthorizeConsumer && unauthorizedScopes.consumer - ? [ScopeType.Consumer, consumer] - : [ScopeType.Producer, ruleType.producer]; - - throw Boom.forbidden( - getUnauthorizedMessage( - ruleTypeId, - unauthorizedScopeType, - unauthorizedScope, - operation, - entity - ) - ); + throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, entity)); } } else if (!isAvailableConsumer) { - throw Boom.forbidden( - getUnauthorizedMessage(ruleTypeId, ScopeType.Consumer, consumer, operation, entity) - ); + throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, entity)); } } @@ -300,13 +256,7 @@ export class AlertingAuthorization { ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( - getUnauthorizedMessage( - ruleTypeId, - ScopeType.Consumer, - consumer, - 'find', - authorizationEntity - ) + getUnauthorizedMessage(ruleTypeId, consumer, 'find', authorizationEntity) ); } else { if (authorizedEntries.has(ruleTypeId)) { @@ -460,19 +410,11 @@ function asAuthorizedConsumers( return fromPairs(consumers.map((feature) => [feature, hasPrivileges])); } -enum ScopeType { - Consumer, - Producer, -} - function getUnauthorizedMessage( alertTypeId: string, - scopeType: ScopeType, scope: string, operation: string, entity: string ): string { - return `Unauthorized to ${operation} a "${alertTypeId}" ${entity} ${ - scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` - }`; + return `Unauthorized by "${scope}" to ${operation} "${alertTypeId}" ${entity}`; } diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 09681f01da2d6..07fd25390de1f 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -12,12 +12,20 @@ import { LicensingPluginSetup, LicensingApiRequestHandlerContext, } from '@kbn/licensing-plugin/server'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; import { ApmRuleType, APM_SERVER_FEATURE_ID, } from '../common/rules/apm_rule_types'; import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '../common/apm_saved_object_constants'; +const ruleTypes = [ + ...Object.values(ApmRuleType), + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ES_QUERY_ID, +]; + export const APM_FEATURE = { id: APM_SERVER_FEATURE_ID, name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { @@ -30,7 +38,7 @@ export const APM_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: Object.values(ApmRuleType), + alerting: ruleTypes, // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { @@ -43,10 +51,10 @@ export const APM_FEATURE = { }, alerting: { alert: { - all: Object.values(ApmRuleType), + all: ruleTypes, }, rule: { - all: Object.values(ApmRuleType), + all: ruleTypes, }, }, management: { @@ -64,10 +72,10 @@ export const APM_FEATURE = { }, alerting: { alert: { - read: Object.values(ApmRuleType), + read: ruleTypes, }, rule: { - read: Object.values(ApmRuleType), + read: ruleTypes, }, }, management: { diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index a81771fbd1f6b..8256fcf46147c 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -96,7 +96,8 @@ "@kbn/logs-shared-plugin", "@kbn/unified-field-list", "@kbn/discover-plugin", - "@kbn/observability-ai-assistant-plugin" + "@kbn/observability-ai-assistant-plugin", + "@kbn/stack-alerts-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index e9fec4a5b4f5d..3c95fb71324c0 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { logViewSavedObjectName } from '@kbn/logs-shared-plugin/server'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, @@ -16,6 +18,13 @@ import { import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { infraSourceConfigurationSavedObjectName } from './lib/sources/saved_object_type'; +const metricRuleTypes = [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +]; + export const METRICS_FEATURE = { id: METRICS_FEATURE_ID, name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { @@ -28,7 +37,7 @@ export const METRICS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + alerting: metricRuleTypes, privileges: { all: { app: ['infra', 'metrics', 'kibana'], @@ -40,10 +49,10 @@ export const METRICS_FEATURE = { }, alerting: { rule: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + all: metricRuleTypes, }, alert: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + all: metricRuleTypes, }, }, management: { @@ -61,10 +70,10 @@ export const METRICS_FEATURE = { }, alerting: { rule: { - read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: metricRuleTypes, }, alert: { - read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: metricRuleTypes, }, }, management: { @@ -75,6 +84,12 @@ export const METRICS_FEATURE = { }, }; +const logsRuleTypes = [ + LOG_DOCUMENT_COUNT_RULE_TYPE_ID, + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +]; + export const LOGS_FEATURE = { id: LOGS_FEATURE_ID, name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { @@ -87,7 +102,7 @@ export const LOGS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], + alerting: logsRuleTypes, privileges: { all: { app: ['infra', 'logs', 'kibana'], @@ -99,10 +114,10 @@ export const LOGS_FEATURE = { }, alerting: { rule: { - all: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], + all: logsRuleTypes, }, alert: { - all: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], + all: logsRuleTypes, }, }, management: { @@ -116,10 +131,10 @@ export const LOGS_FEATURE = { api: ['infra', 'rac'], alerting: { rule: { - read: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], + read: logsRuleTypes, }, alert: { - read: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], + read: logsRuleTypes, }, }, management: { diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 519e2a8a1e941..31643d4a1235b 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -69,7 +69,8 @@ "@kbn/logs-shared-plugin", "@kbn/licensing-plugin", "@kbn/aiops-utils", - "@kbn/lens-embeddable-utils" + "@kbn/lens-embeddable-utils", + "@kbn/stack-alerts-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability/common/constants.ts b/x-pack/plugins/observability/common/constants.ts index 3ba31bb1ee1eb..20e121595489e 100644 --- a/x-pack/plugins/observability/common/constants.ts +++ b/x-pack/plugins/observability/common/constants.ts @@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n'; import { AlertConsumers } from '@kbn/rule-data-utils'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; +import type { RuleCreationValidConsumer } from '@kbn/triggers-actions-ui-plugin/public'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/stack-alerts-plugin/common'; export const SLO_BURN_RATE_RULE_TYPE_ID = 'slo.rules.burnRate'; export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; @@ -56,3 +58,12 @@ export const observabilityAlertFeatureIds: ValidFeatureId[] = [ AlertConsumers.SLO, AlertConsumers.OBSERVABILITY, ]; + +export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[] = [ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + AlertConsumers.SLO, + STACK_ALERTS_FEATURE_ID, +]; diff --git a/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts b/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts index d72118182ed65..9ed239f507ea2 100644 --- a/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts +++ b/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts @@ -6,10 +6,13 @@ */ import { useMemo } from 'react'; +import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; import { usePluginContext } from './use_plugin_context'; export function useGetFilteredRuleTypes() { const { observabilityRuleTypeRegistry } = usePluginContext(); - return useMemo(() => observabilityRuleTypeRegistry.list(), [observabilityRuleTypeRegistry]); + return useMemo(() => { + return [...observabilityRuleTypeRegistry.list(), ES_QUERY_ID]; + }, [observabilityRuleTypeRegistry]); } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index cefc17fa87de6..cd8da494308a2 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -18,6 +18,7 @@ import { useKibana } from '../../utils/kibana_react'; import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeBuckets } from '../../hooks/use_time_buckets'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { useToasts } from '../../hooks/use_toast'; import { LoadingObservability } from '../../components/loading_observability'; import { renderRuleStats, RuleStatsState } from './components/rule_stats'; @@ -57,11 +58,13 @@ function InternalAlertsPage() { getAlertSummaryWidget: AlertSummaryWidget, }, } = useKibana().services; - const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); + const { ObservabilityPageTemplate } = usePluginContext(); const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, { replace: false, }); + const filteredRuleTypes = useGetFilteredRuleTypes(); + const onBrushEnd: BrushEndListener = (brushEvent) => { const { x } = brushEvent as XYBrushEvent; if (x) { @@ -124,7 +127,7 @@ function InternalAlertsPage() { try { const response = await loadRuleAggregations({ http, - typesFilter: observabilityRuleTypeRegistry.list(), + typesFilter: filteredRuleTypes, }); const { ruleExecutionStatus, ruleMutedStatus, ruleEnabledStatus, ruleSnoozedStatus } = response; diff --git a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx index fcc562111dfe5..b01218e47e61c 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -17,6 +17,7 @@ import { useKibana } from '../../utils/kibana_react'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; import { useFetchRuleTypes } from '../../hooks/use_fetch_rule_types'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { PageTitle } from './components/page_title'; import { DeleteConfirmationModal } from './components/delete_confirmation_modal'; import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; @@ -64,7 +65,7 @@ export function RuleDetailsPage() { getRuleStatusPanel: RuleStatusPanel, }, } = useKibana().services; - const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); + const { ObservabilityPageTemplate } = usePluginContext(); const { ruleId } = useParams(); const { search } = useLocation(); @@ -74,8 +75,9 @@ export function RuleDetailsPage() { const { rule, isLoading, isError, refetch } = useFetchRule({ ruleId }); + const filteredRuleTypes = useGetFilteredRuleTypes(); const { ruleTypes } = useFetchRuleTypes({ - filterByRuleTypeIds: observabilityRuleTypeRegistry.list(), + filterByRuleTypeIds: filteredRuleTypes, }); useBreadcrumbs([ diff --git a/x-pack/plugins/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability/public/pages/rules/rules.tsx index 2de5ebd13bd45..011f0500e8efa 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.tsx @@ -18,6 +18,7 @@ import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../utils/kibana_react'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; +import { observabilityRuleCreationValidConsumers } from '../../../common/constants'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; export function RulesPage() { @@ -177,6 +178,7 @@ export function RulesPage() { { setAddRuleFlyoutVisibility(false); }} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 72656db6dcdc4..d13c666b026f2 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -22,7 +22,10 @@ import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; import { rulesLocatorID, sloFeatureId } from '../../../../common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../../../common/constants'; +import { + SLO_BURN_RATE_RULE_TYPE_ID, + observabilityRuleCreationValidConsumers, +} from '../../../../common/constants'; import { sloKeys } from '../../../hooks/slo/query_key_factory'; import { useCapabilities } from '../../../hooks/slo/use_capabilities'; import { useCloneSlo } from '../../../hooks/slo/use_clone_slo'; @@ -273,6 +276,7 @@ export function SloListItem({ filteredRuleTypes={filteredRuleTypes} ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID} initialValues={{ name: `${slo.name} Burn Rate rule`, params: { sloId: slo.id } }} + validConsumers={observabilityRuleCreationValidConsumers} onSave={handleSavedRule} onClose={() => { setIsAddRuleFlyoutOpen(false); diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index d5d5604001aa9..408f0dbf209d8 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -47,7 +47,7 @@ const configSchema = schema.object({ }), }), thresholdRule: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), }), }), thresholdRule: schema.object({ diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 0b0b7207d69ec..a6ea3d09473ae 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -26,9 +26,13 @@ import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/serve import { SharePluginSetup } from '@kbn/share-plugin/server'; import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; import { ObservabilityConfig } from '.'; import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants'; +import { + SLO_BURN_RATE_RULE_TYPE_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +} from '../common/constants'; import { kubernetesGuideConfig, kubernetesGuideId, @@ -70,6 +74,8 @@ interface PluginStart { alerting: PluginStartContract; } +const ruleTypes = [SLO_BURN_RATE_RULE_TYPE_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, ES_QUERY_ID]; + export class ObservabilityPlugin implements Plugin { private logger: Logger; @@ -192,7 +198,7 @@ export class ObservabilityPlugin implements Plugin { category: DEFAULT_APP_CATEGORIES.observability, app: [sloFeatureId, 'kibana'], catalogue: [sloFeatureId, 'observability'], - alerting: [SLO_BURN_RATE_RULE_TYPE_ID], + alerting: ruleTypes, privileges: { all: { app: [sloFeatureId, 'kibana'], @@ -204,10 +210,10 @@ export class ObservabilityPlugin implements Plugin { }, alerting: { rule: { - all: [SLO_BURN_RATE_RULE_TYPE_ID], + all: ruleTypes, }, alert: { - all: [SLO_BURN_RATE_RULE_TYPE_ID], + all: ruleTypes, }, }, ui: ['read', 'write'], @@ -222,10 +228,10 @@ export class ObservabilityPlugin implements Plugin { }, alerting: { rule: { - read: [SLO_BURN_RATE_RULE_TYPE_ID], + read: ruleTypes, }, alert: { - read: [SLO_BURN_RATE_RULE_TYPE_ID], + read: ruleTypes, }, }, ui: ['read'], diff --git a/x-pack/plugins/stack_alerts/common/constants.ts b/x-pack/plugins/stack_alerts/common/constants.ts index cac00873face2..38b0a2c6e73e3 100644 --- a/x-pack/plugins/stack_alerts/common/constants.ts +++ b/x-pack/plugins/stack_alerts/common/constants.ts @@ -6,3 +6,4 @@ */ export const STACK_ALERTS_FEATURE_ID = 'stackAlerts'; +export const ES_QUERY_ID = '.es-query'; diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 4a9be641712f1..58d2c6022e798 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -11,4 +11,4 @@ export { ComparatorFnNames, getHumanReadableComparator, } from './comparator'; -export { STACK_ALERTS_FEATURE_ID } from './constants'; +export { STACK_ALERTS_FEATURE_ID, ES_QUERY_ID } from './constants'; diff --git a/x-pack/plugins/stack_alerts/kibana.jsonc b/x-pack/plugins/stack_alerts/kibana.jsonc index 668d38d291a34..7333de149470b 100644 --- a/x-pack/plugins/stack_alerts/kibana.jsonc +++ b/x-pack/plugins/stack_alerts/kibana.jsonc @@ -23,6 +23,7 @@ ], "requiredBundles": [ "esUiShared" - ] + ], + "extraPublicDirs": ["common"] } } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts index 700cba4680bff..9627249c2e6e1 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts @@ -5,6 +5,6 @@ * 2.0. */ -export const ES_QUERY_ID = '.es-query'; export const ActionGroupId = 'query matched'; export const ConditionMetAlertInstanceId = 'query matched'; +export { ES_QUERY_ID } from '../../../common'; diff --git a/x-pack/plugins/synthetics/server/feature.ts b/x-pack/plugins/synthetics/server/feature.ts index 265def5c3fa01..576b48b4e87ec 100644 --- a/x-pack/plugins/synthetics/server/feature.ts +++ b/x-pack/plugins/synthetics/server/feature.ts @@ -6,6 +6,8 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; @@ -19,6 +21,14 @@ const UPTIME_RULE_TYPES = [ 'xpack.uptime.alerts.monitorStatus', 'xpack.uptime.alerts.durationAnomaly', ]; + +const ruleTypes = [ + ...UPTIME_RULE_TYPES, + ...SYNTHETICS_RULE_TYPES, + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +]; + export const uptimeFeature = { id: PLUGIN.ID, name: PLUGIN.NAME, @@ -29,7 +39,7 @@ export const uptimeFeature = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES], + alerting: ruleTypes, privileges: { all: { app: ['uptime', 'kibana', 'synthetics'], @@ -47,10 +57,10 @@ export const uptimeFeature = { }, alerting: { rule: { - all: [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES], + all: ruleTypes, }, alert: { - all: [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES], + all: ruleTypes, }, }, management: { @@ -74,10 +84,10 @@ export const uptimeFeature = { }, alerting: { rule: { - read: [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES], + read: ruleTypes, }, alert: { - read: [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES], + read: ruleTypes, }, }, management: { diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index 12bc371fef915..4862aa4729aa1 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -77,6 +77,7 @@ "@kbn/core-saved-objects-server-mocks", "@kbn/shared-ux-page-kibana-template", "@kbn/observability-ai-assistant-plugin", + "@kbn/stack-alerts-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 22ead5ca4d2fe..48476a259683d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; - export { BASE_ALERTING_API_PATH, INTERNAL_BASE_ALERTING_API_PATH, @@ -15,6 +14,9 @@ export { BASE_ACTION_API_PATH, INTERNAL_BASE_ACTION_API_PATH } from '@kbn/action export type Section = 'connectors' | 'rules' | 'alerts' | 'logs'; +export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; +export const ES_QUERY_RULE_TYPE_ID = '.es-query'; + export const routeToHome = `/`; export const routeToConnectors = `/connectors`; export const routeToRules = `/rules`; @@ -116,3 +118,8 @@ export const CONNECTOR_LOCKED_COLUMNS = ['timestamp', 'status', 'connector_name' export const GLOBAL_CONNECTOR_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [ ...CONNECTOR_LOCKED_COLUMNS, ]; + +export const MULTI_CONSUMER_RULE_TYPE_IDS = [ + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ES_QUERY_RULE_TYPE_ID, +]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index a995d697e8c54..c74b4b3fd6b90 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -29,8 +29,20 @@ export function hasAllPrivilege( ): boolean { return ruleType?.authorizedConsumers[ruleConsumer]?.all ?? false; } + +export function hasAllPrivilegeWithProducerCheck( + ruleConsumer: InitialRule['consumer'], + ruleType?: RuleType +): boolean { + if (ruleConsumer === ruleType?.producer) { + return true; + } + return hasAllPrivilege(ruleConsumer, ruleType); +} + export function hasReadPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { return ruleType?.authorizedConsumers[rule.consumer]?.read ?? false; } + export const hasManageApiKeysCapability = (capabilities: Capabilities) => capabilities?.management?.security?.api_keys; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index 92a1329b0e67a..4861fb77393c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -16,12 +16,16 @@ import RuleAdd from './rule_add'; import { createRule } from '../../lib/rule_api/create'; import { alertingFrameworkHealth } from '../../lib/rule_api/health'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { Rule, RuleAddProps, RuleFlyoutCloseReason, GenericValidationResult, ValidationResult, + RuleCreationValidConsumer, + RuleType, + RuleTypeModel, } from '../../../types'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { ReactWrapper } from 'enzyme'; @@ -84,17 +88,30 @@ describe('rule_add', () => { }); let wrapper: ReactWrapper; - async function setup( - initialValues?: Partial, - onClose: RuleAddProps['onClose'] = jest.fn(), - defaultScheduleInterval?: string, - ruleTypeId?: string, - actionsShow: boolean = false - ) { + async function setup({ + initialValues, + onClose = jest.fn(), + defaultScheduleInterval, + ruleTypeId, + actionsShow = false, + validConsumers, + ruleTypesOverwrite, + ruleTypeModelOverwrite, + }: { + initialValues?: Partial; + onClose?: RuleAddProps['onClose']; + defaultScheduleInterval?: string; + ruleTypeId?: string; + actionsShow?: boolean; + validConsumers?: RuleCreationValidConsumer[]; + ruleTypesOverwrite?: RuleType[]; + ruleTypeModelOverwrite?: RuleTypeModel; + }) { const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types'); - const ruleTypes = [ + + const ruleTypes = ruleTypesOverwrite || [ { id: 'my-rule-type', name: 'Test', @@ -144,7 +161,7 @@ describe('rule_add', () => { hasPermanentEncryptionKey: true, }); - const ruleType = { + const ruleType = ruleTypeModelOverwrite || { id: 'my-rule-type', iconClass: 'test', description: 'test', @@ -186,6 +203,7 @@ describe('rule_add', () => { ruleTypeRegistry={ruleTypeRegistry} metadata={{ test: 'some value', fields: ['test'] }} ruleTypeId={ruleTypeId} + validConsumers={validConsumers} /> ); @@ -201,7 +219,10 @@ describe('rule_add', () => { minimumScheduleInterval: { value: '1m', enforce: false }, }); const onClose = jest.fn(); - await setup({}, onClose); + await setup({ + initialValues: {}, + onClose, + }); await act(async () => { await nextTick(); @@ -223,7 +244,10 @@ describe('rule_add', () => { minimumScheduleInterval: { value: '1m', enforce: false }, }); const onClose = jest.fn(); - await setup({}, onClose); + await setup({ + initialValues: {}, + onClose, + }); await act(async () => { await nextTick(); @@ -245,8 +269,8 @@ describe('rule_add', () => { minimumScheduleInterval: { value: '1m', enforce: false }, }); const onClose = jest.fn(); - await setup( - { + await setup({ + initialValues: { name: 'Simple status rule', tags: ['uptime', 'logs'], schedule: { @@ -254,9 +278,8 @@ describe('rule_add', () => { }, }, onClose, - undefined, - 'my-rule-type' - ); + ruleTypeId: 'my-rule-type', + }); expect(wrapper.find('input#ruleName').props().value).toBe('Simple status rule'); expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('uptimelogs'); @@ -266,7 +289,7 @@ describe('rule_add', () => { it('renders rule add flyout with DEFAULT_RULE_INTERVAL if no initialValues specified and no minimumScheduleInterval', async () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({}); - await setup(undefined, undefined, undefined, 'my-rule-type'); + await setup({ ruleTypeId: 'my-rule-type' }); expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(1); expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m'); @@ -276,7 +299,7 @@ describe('rule_add', () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ minimumScheduleInterval: { value: '5m', enforce: false }, }); - await setup(undefined, undefined, undefined, 'my-rule-type'); + await setup({ ruleTypeId: 'my-rule-type' }); expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(5); expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m'); @@ -291,8 +314,8 @@ describe('rule_add', () => { (createRule as jest.MockedFunction).mockResolvedValue(rule); - await setup( - { + await setup({ + initialValues: { name: 'Simple status rule', ruleTypeId: 'my-rule-type', tags: ['uptime', 'logs'], @@ -300,8 +323,8 @@ describe('rule_add', () => { interval: '1h', }, }, - onClose - ); + onClose, + }); wrapper.find('[data-test-subj="saveRuleButton"]').last().simulate('click'); @@ -317,11 +340,195 @@ describe('rule_add', () => { }); }); + it('should set consumer when the consumer selection renders and rule is created', async () => { + (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ + minimumScheduleInterval: { value: '1m', enforce: false }, + }); + const onClose = jest.fn(); + await setup({ + initialValues: { + name: 'Simple rule', + consumer: 'alerts', + ruleTypeId: 'observability.rules.threshold', + tags: ['uptime', 'logs'], + schedule: { + interval: '1h', + }, + }, + onClose, + ruleTypesOverwrite: [ + { + id: 'observability.rules.threshold', + name: 'Threshold Rule', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + enabledInLicense: true, + defaultActionGroupId: 'threshold.fired', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + alerts: { read: true, all: true }, + apm: { read: true, all: true }, + discover: { read: true, all: true }, + infrastructure: { read: true, all: true }, + logs: { read: true, all: true }, + ml: { read: true, all: true }, + monitoring: { read: true, all: true }, + siem: { read: true, all: true }, + // Setting SLO all to false, this shouldn't show up + slo: { read: true, all: false }, + stackAlerts: { read: true, all: true }, + uptime: { read: true, all: true }, + }, + actionVariables: { + context: [], + state: [], + params: [], + }, + }, + ], + ruleTypeModelOverwrite: { + id: 'observability.rules.threshold', + iconClass: 'test', + description: 'test', + documentationUrl: null, + validate: (): ValidationResult => { + return { errors: {} }; + }, + ruleParamsExpression: TestExpression, + requiresAppContext: false, + }, + validConsumers: [ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + AlertConsumers.SLO, + ], + }); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + wrapper.find('[data-test-subj="saveRuleButton"]').last().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(createRule).toHaveBeenLastCalledWith( + expect.objectContaining({ + rule: expect.objectContaining({ + consumer: 'apm', + }), + }) + ); + }); + + it('should set consumer automatically if only 1 authorized consumer exists', async () => { + (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ + minimumScheduleInterval: { value: '1m', enforce: false }, + }); + const onClose = jest.fn(); + await setup({ + initialValues: { + name: 'Simple rule', + consumer: 'alerts', + ruleTypeId: 'observability.rules.threshold', + tags: ['uptime', 'logs'], + schedule: { + interval: '1h', + }, + }, + onClose, + ruleTypesOverwrite: [ + { + id: 'observability.rules.threshold', + name: 'Threshold Rule', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + enabledInLicense: true, + defaultActionGroupId: 'threshold.fired', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + logs: { read: true, all: true }, + }, + actionVariables: { + context: [], + state: [], + params: [], + }, + }, + ], + ruleTypeModelOverwrite: { + id: 'observability.rules.threshold', + iconClass: 'test', + description: 'test', + documentationUrl: null, + validate: (): ValidationResult => { + return { errors: {} }; + }, + ruleParamsExpression: TestExpression, + requiresAppContext: false, + }, + validConsumers: [ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + AlertConsumers.SLO, + ], + }); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="addRuleFlyoutTitle"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy(); + + wrapper.find('[data-test-subj="saveRuleButton"]').last().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(createRule).toHaveBeenLastCalledWith( + expect.objectContaining({ + rule: expect.objectContaining({ + consumer: 'logs', + }), + }) + ); + }); + it('should enforce any default interval', async () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false }, }); - await setup({ ruleTypeId: 'my-rule-type' }, jest.fn(), '3h', 'my-rule-type', true); + await setup({ + initialValues: { ruleTypeId: 'my-rule-type' }, + onClose: jest.fn(), + defaultScheduleInterval: '3h', + ruleTypeId: 'my-rule-type', + actionsShow: true, + }); // Wait for handlers to fire await act(async () => { @@ -344,7 +551,12 @@ describe('rule_add', () => { minimumScheduleInterval: { value: '1m', enforce: false }, }); - await setup({}, jest.fn(), undefined, 'my-rule-type', true); + await setup({ + initialValues: {}, + onClose: jest.fn(), + ruleTypeId: 'my-rule-type', + actionsShow: true, + }); expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1); expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1); @@ -361,7 +573,12 @@ describe('rule_add', () => { hasPermanentEncryptionKey: false, }); - await setup({}, jest.fn(), undefined, 'my-rule-type', true); + await setup({ + initialValues: {}, + onClose: jest.fn(), + ruleTypeId: 'my-rule-type', + actionsShow: true, + }); expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1); expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index d27abe0831441..7534b968dc981 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -19,6 +19,7 @@ import { RuleAddProps, RuleTypeIndex, TriggersActionsUiConfig, + RuleCreationValidConsumer, } from '../../../types'; import { RuleForm } from './rule_form'; import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; @@ -51,6 +52,7 @@ const RuleAdd = ({ hideInterval, metadata: initialMetadata, filteredRuleTypes, + validConsumers, ...props }: RuleAddProps) => { const onSaveHandler = onSave ?? reloadRules; @@ -83,6 +85,7 @@ const RuleAdd = ({ props.ruleTypeIndex ); const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); + const [selectedConsumer, setSelectedConsumer] = useState(); const setRule = (value: InitialRule) => { dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); @@ -207,7 +210,13 @@ const RuleAdd = ({ async function onSaveRule(): Promise { try { - const newRule = await createRule({ http, rule: rule as RuleUpdates }); + const newRule = await createRule({ + http, + rule: { + ...rule, + ...(selectedConsumer ? { consumer: selectedConsumer } : {}), + } as RuleUpdates, + }); toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText', { defaultMessage: 'Created rule "{ruleName}"', @@ -250,6 +259,7 @@ const RuleAdd = ({ = () => { + return ( + + + + ); +}; + jest.mock('../../hooks/use_load_rule_types', () => ({ useLoadRuleTypes: jest.fn(), })); @@ -233,15 +252,30 @@ describe('rule_form', () => { describe('rule_form create rule', () => { let wrapper: ReactWrapper; - async function setup( - showRulesList = false, - enforceMinimum = false, - schedule = '1m', - featureId = 'alerting' - ) { + async function setup(options?: { + showRulesList?: boolean; + enforceMinimum?: boolean; + schedule?: string; + featureId?: string; + initialRuleOverwrite?: Partial; + validConsumers?: RuleCreationValidConsumer[]; + ruleTypesOverwrite?: RuleType[]; + ruleTypeModelOverwrite?: RuleTypeModel; + }) { + const { + showRulesList = false, + enforceMinimum = false, + schedule = '1m', + featureId = 'alerting', + initialRuleOverwrite, + validConsumers, + ruleTypesOverwrite, + ruleTypeModelOverwrite, + } = options || {}; + const mocks = coreMock.createSetup(); const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types'); - const ruleTypes: RuleType[] = [ + const ruleTypes: RuleType[] = ruleTypesOverwrite || [ { id: 'my-rule-type', name: 'Test', @@ -305,7 +339,7 @@ describe('rule_form', () => { }, }; ruleTypeRegistry.list.mockReturnValue([ - ruleType, + ruleTypeModelOverwrite || ruleType, ruleTypeNonEditable, disabledByLicenseRuleType, ]); @@ -330,7 +364,11 @@ describe('rule_form', () => { wrapper = mountWithIntl( { ruleTypeRegistry={ruleTypeRegistry} connectorFeatureId={featureId} onChangeMetaData={jest.fn()} + validConsumers={validConsumers} + setConsumer={mockSetConsumer} /> ); @@ -359,20 +399,20 @@ describe('rule_form', () => { }); it('renders registered selected rule type', async () => { - await setup(true); + await setup({ showRulesList: true }); const ruleTypeSelectOptions = wrapper.find('[data-test-subj="my-rule-type-SelectOption"]'); expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); it('renders minimum schedule interval helper text when enforce = true', async () => { - await setup(false, true); + await setup({ enforceMinimum: true }); expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( `Interval must be at least 1 minute.` ); }); it('renders minimum schedule interval helper suggestion when enforce = false and schedule is less than configuration', async () => { - await setup(false, false, '10s'); + await setup({ schedule: '10s' }); expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( `Intervals less than 1 minute are not recommended due to performance considerations.` ); @@ -440,7 +480,7 @@ describe('rule_form', () => { }); it('renders uses feature id to load action types', async () => { - await setup(false, false, '1m', 'anotherFeature'); + await setup({ schedule: '1m', featureId: 'anotherFeature' }); const ruleTypeSelectOptions = wrapper.find( '[data-test-subj=".server-log-anotherFeature-ActionTypeSelectOption"]' ); @@ -448,7 +488,7 @@ describe('rule_form', () => { }); it('renders rule type description', async () => { - await setup(true); + await setup({ showRulesList: true }); wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); expect(ruleDescription.exists()).toBeTruthy(); @@ -456,7 +496,7 @@ describe('rule_form', () => { }); it('renders rule type documentation link', async () => { - await setup(true); + await setup({ showRulesList: true }); wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); expect(ruleDocumentationLink.exists()).toBeTruthy(); @@ -464,13 +504,174 @@ describe('rule_form', () => { }); it('renders rule types disabled by license', async () => { - await setup(true); + await setup({ showRulesList: true }); const actionOption = wrapper.find(`[data-test-subj="disabled-by-license-SelectOption"]`); expect(actionOption.exists()).toBeTruthy(); expect( wrapper.find('[data-test-subj="disabled-by-license-disabledTooltip"]').exists() ).toBeTruthy(); }); + + it('should display the consumer select for specific rule types', async () => { + await setup({ + initialRuleOverwrite: { + name: 'Simple rule', + consumer: 'alerts', + ruleTypeId: 'observability.rules.threshold', + schedule: { + interval: '1h', + }, + }, + ruleTypesOverwrite: [ + { + id: 'observability.rules.threshold', + name: 'Threshold Rule', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + enabledInLicense: true, + defaultActionGroupId: 'threshold.fired', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + alerts: { read: true, all: true }, + apm: { read: true, all: true }, + discover: { read: true, all: true }, + infrastructure: { read: true, all: true }, + logs: { read: true, all: true }, + ml: { read: true, all: true }, + monitoring: { read: true, all: true }, + siem: { read: true, all: true }, + // Setting SLO all to false, this shouldn't show up + slo: { read: true, all: false }, + stackAlerts: { read: true, all: true }, + uptime: { read: true, all: true }, + }, + actionVariables: { + context: [], + state: [], + params: [], + }, + }, + ], + ruleTypeModelOverwrite: { + id: 'observability.rules.threshold', + iconClass: 'test', + description: 'test', + documentationUrl: null, + validate: (): ValidationResult => { + return { errors: {} }; + }, + ruleParamsExpression: TestExpression, + requiresAppContext: false, + }, + validConsumers: [ + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.UPTIME, + AlertConsumers.SLO, + ], + }); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeTruthy(); + expect(wrapper.find(RuleFormConsumerSelection).props().consumers).toEqual([ + 'apm', + 'infrastructure', + 'logs', + 'uptime', + ]); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(mockSetConsumer).toHaveBeenLastCalledWith('apm'); + }); + + it('should not display the consumer select if there is only 1 consumer', async () => { + await setup({ + initialRuleOverwrite: { + name: 'Simple rule', + consumer: 'alerts', + ruleTypeId: 'observability.rules.threshold', + schedule: { + interval: '1h', + }, + }, + ruleTypesOverwrite: [ + { + id: 'observability.rules.threshold', + name: 'Threshold Rule', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + enabledInLicense: true, + defaultActionGroupId: 'threshold.fired', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + apm: { read: true, all: true }, + }, + actionVariables: { + context: [], + state: [], + params: [], + }, + }, + ], + ruleTypeModelOverwrite: { + id: 'observability.rules.threshold', + iconClass: 'test', + description: 'test', + documentationUrl: null, + validate: (): ValidationResult => { + return { errors: {} }; + }, + ruleParamsExpression: TestExpression, + requiresAppContext: false, + }, + }); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(mockSetConsumer).toHaveBeenLastCalledWith('apm'); + }); + + it('should not display the consumer select for invalid rule types', async () => { + await setup(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); + }); }); describe('rule_form create rule non ruleing consumer and producer', () => { @@ -605,14 +806,6 @@ describe('rule_form', () => { ); expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); - - it('does not render rule type options which producer does not correspond to the rule consumer', async () => { - await setup(); - const ruleTypeSelectOptions = wrapper.find( - '[data-test-subj="other-consumer-producer-rule-type-SelectOption"]' - ); - expect(ruleTypeSelectOptions.exists()).toBeFalsy(); - }); }); describe('rule_form edit rule', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index c72dced42a2d9..dfd7a807b6134 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useCallback, Suspense } from 'react'; +import React, { Fragment, useState, useEffect, useCallback, Suspense, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -61,6 +61,7 @@ import { RuleTypeRegistryContract, ActionTypeRegistryContract, TriggersActionsUiConfig, + RuleCreationValidConsumer, } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { ActionForm } from '../action_connector_form'; @@ -73,7 +74,9 @@ import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_ena import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; +import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; import { SectionLoading } from '../../components/section_loading'; +import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection'; import { useLoadRuleTypes } from '../../hooks/use_load_rule_types'; import { getInitialInterval } from './get_initial_interval'; @@ -81,10 +84,44 @@ const ENTER_KEY = 13; const INTEGER_REGEX = /^[1-9][0-9]*$/; +const NOOP = () => {}; + function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } +const authorizedToDisplayRuleType = ({ + rule, + ruleType, + validConsumers, +}: { + rule: InitialRule; + ruleType: RuleType; + validConsumers?: RuleCreationValidConsumer[]; +}) => { + if (!ruleType) { + return false; + } + // If we have a generic threshold/ES query rule... + if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id)) { + // And an array of valid consumers are passed in, we will show it + // if the rule type has at least one of the consumers as authorized + if (Array.isArray(validConsumers)) { + return validConsumers.some((consumer) => hasAllPrivilege(consumer, ruleType)); + } + // If no array was passed in, then we will show it if at least one of its + // authorized consumers allows it to be shown. + return Object.entries(ruleType.authorizedConsumers).some(([_, privilege]) => { + return privilege.all; + }); + } + // For non-generic threshold/ES query rules, we will still do the check + // against `alerts` since we are still setting rule consumers to `alerts` + return hasAllPrivilege(rule.consumer, ruleType); +}; + +export type RuleTypeItems = Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }>; + interface RuleFormProps> { rule: InitialRule; config: TriggersActionsUiConfig; @@ -94,12 +131,15 @@ interface RuleFormProps> { actionTypeRegistry: ActionTypeRegistryContract; operation: string; canChangeTrigger?: boolean; // to hide Change trigger button + canShowConsumerSelection?: boolean; setHasActionsDisabled?: (value: boolean) => void; setHasActionsWithBrokenConnector?: (value: boolean) => void; + setConsumer?: (consumer: RuleCreationValidConsumer) => void; metadata?: MetaData; filteredRuleTypes?: string[]; hideInterval?: boolean; connectorFeatureId?: string; + validConsumers?: RuleCreationValidConsumer[]; onChangeMetaData: (metadata: MetaData) => void; } @@ -107,10 +147,12 @@ export const RuleForm = ({ rule, config, canChangeTrigger = true, + canShowConsumerSelection = false, dispatch, errors, setHasActionsDisabled, setHasActionsWithBrokenConnector, + setConsumer = NOOP, operation, ruleTypeRegistry, actionTypeRegistry, @@ -118,6 +160,7 @@ export const RuleForm = ({ filteredRuleTypes: ruleTypeToFilter, hideInterval, connectorFeatureId = AlertingConnectorFeatureId, + validConsumers, onChangeMetaData, }: RuleFormProps) => { const { @@ -150,12 +193,8 @@ export const RuleForm = ({ ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); - const [availableRuleTypes, setAvailableRuleTypes] = useState< - Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }> - >([]); - const [filteredRuleTypes, setFilteredRuleTypes] = useState< - Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }> - >([]); + const [availableRuleTypes, setAvailableRuleTypes] = useState([]); + const [filteredRuleTypes, setFilteredRuleTypes] = useState([]); const [searchText, setSearchText] = useState(); const [inputText, setInputText] = useState(); const [solutions, setSolutions] = useState | undefined>(undefined); @@ -177,23 +216,23 @@ export const RuleForm = ({ const getAvailableRuleTypes = (ruleTypesResult: RuleType[]) => ruleTypeRegistry .list() - .reduce( - ( - arr: Array<{ ruleType: RuleType; ruleTypeModel: RuleTypeModel }>, - ruleTypeRegistryItem: RuleTypeModel - ) => { - const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); - if (ruleType) { - arr.push({ - ruleType, - ruleTypeModel: ruleTypeRegistryItem, - }); - } - return arr; - }, - [] + .reduce((arr: RuleTypeItems, ruleTypeRegistryItem: RuleTypeModel) => { + const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); + if (ruleType) { + arr.push({ + ruleType, + ruleTypeModel: ruleTypeRegistryItem, + }); + } + return arr; + }, []) + .filter(({ ruleType }) => + authorizedToDisplayRuleType({ + rule, + ruleType, + validConsumers, + }) ) - .filter((item) => item.ruleType && hasAllPrivilege(rule.consumer, item.ruleType)) .filter((item) => rule.consumer === ALERTS_FEATURE_ID ? !item.ruleTypeModel.requiresAppContext @@ -221,7 +260,16 @@ export const RuleForm = ({ setSolutions( new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b))) ); - }, [ruleTypes, ruleTypeIndex, rule.ruleTypeId, kibanaFeatures, rule.consumer, ruleTypeRegistry]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + ruleTypes, + ruleTypeIndex, + rule.ruleTypeId, + kibanaFeatures, + rule.consumer, + ruleTypeRegistry, + validConsumers, + ]); useEffect(() => { if (loadRuleTypesError) { @@ -319,6 +367,44 @@ export const RuleForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ruleTypeRegistry, availableRuleTypes, searchText, JSON.stringify(solutionsFilter)]); + const authorizedConsumers = useMemo(() => { + // If the app context provides a consumer, we assume that consumer is + // is what we set for all rules that is created in that context + if (rule.consumer !== ALERTS_FEATURE_ID) { + return []; + } + + const selectedRuleType = availableRuleTypes.find( + ({ ruleType: availableRuleType }) => availableRuleType.id === rule.ruleTypeId + ); + if (!selectedRuleType?.ruleType?.authorizedConsumers) { + return []; + } + return Object.entries(selectedRuleType.ruleType.authorizedConsumers).reduce< + RuleCreationValidConsumer[] + >((result, [authorizedConsumer, privilege]) => { + if ( + privilege.all && + (validConsumers || VALID_CONSUMERS).includes( + authorizedConsumer as RuleCreationValidConsumer + ) + ) { + result.push(authorizedConsumer as RuleCreationValidConsumer); + } + return result; + }, []); + }, [availableRuleTypes, rule, validConsumers]); + + const shouldShowConsumerSelect = useMemo(() => { + if (!canShowConsumerSelection) { + return false; + } + if (!authorizedConsumers.length) { + return false; + } + return !!rule.ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule.ruleTypeId); + }, [authorizedConsumers, rule, canShowConsumerSelection]); + const selectedRuleType = rule?.ruleTypeId ? ruleTypeIndex?.get(rule?.ruleTypeId) : undefined; const recoveryActionGroup = selectedRuleType?.recoveryActionGroup?.id; @@ -639,6 +725,18 @@ export const RuleForm = ({ )} + {shouldShowConsumerSelect && ( + <> + + + + + + )} {canShowActions && defaultActionGroupId && diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx new file mode 100644 index 0000000000000..3ef9a7babe065 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { RuleFormConsumerSelection } from './rule_form_consumer_selection'; +import { RuleCreationValidConsumer } from '../../../types'; + +const mockConsumers: RuleCreationValidConsumer[] = [ + 'logs', + 'infrastructure', + 'apm', + 'uptime', + 'slo', + 'stackAlerts', +]; + +const mockOnChange = jest.fn(); + +describe('RuleFormConsumerSelectionModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + render(); + + expect(screen.getByTestId('ruleFormConsumerSelect')).toBeInTheDocument(); + + expect(screen.getByText('Logs')).toBeInTheDocument(); + expect(screen.getByText('Metrics')).toBeInTheDocument(); + expect(screen.getByText('APM and User Experience')).toBeInTheDocument(); + expect(screen.getByText('Synthetics and Uptime')).toBeInTheDocument(); + expect(screen.getByText('SLOs')).toBeInTheDocument(); + expect(screen.getByText('Stack Rules')).toBeInTheDocument(); + }); + + it('should initialize dropdown if provided with a valid initial consumer', () => { + render(); + + // Selects first option if no initial value is provided + expect(mockOnChange).toHaveBeenLastCalledWith('apm'); + mockOnChange.mockClear(); + + // Selects initial consumer + render( + + ); + + expect(mockOnChange).toHaveBeenLastCalledWith('slo'); + mockOnChange.mockClear(); + + // Selects first value if provided with invalid consumer + render( + + ); + + expect(mockOnChange).toHaveBeenLastCalledWith('apm'); + }); + + it('should select options and call onChange', () => { + render(); + + mockConsumers.forEach((consumer) => { + fireEvent.change(screen.getByTestId('ruleFormConsumerSelect'), { + target: { value: consumer }, + }); + + expect(mockOnChange).toHaveBeenLastCalledWith(consumer); + }); + }); + + it('should default selection to the first option sorted alphabetically', () => { + render(); + + expect(mockOnChange).toHaveBeenLastCalledWith('apm'); + }); + + it('should default to specified consumer if passed in', () => { + render( + + ); + + expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); + }); + + it('should display nothing if there is only 1 consumer to select', () => { + render(); + + expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); + expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx new file mode 100644 index 0000000000000..b2e08493fdcff --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx @@ -0,0 +1,142 @@ +/* + * 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 React, { useMemo, useState, useCallback, useEffect } from 'react'; +import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { RuleCreationValidConsumer } from '../../../types'; + +const SELECT_LABEL: string = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel', + { + defaultMessage: 'Select role visibility', + } +); + +const featureNameMap: Record = { + [AlertConsumers.LOGS]: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs', + { + defaultMessage: 'Logs', + } + ), + [AlertConsumers.INFRASTRUCTURE]: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure', + { + defaultMessage: 'Metrics', + } + ), + [AlertConsumers.APM]: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm', + { + defaultMessage: 'APM and User Experience', + } + ), + [AlertConsumers.UPTIME]: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime', + { + defaultMessage: 'Synthetics and Uptime', + } + ), + [AlertConsumers.SLO]: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo', + { + defaultMessage: 'SLOs', + } + ), + stackAlerts: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts', + { + defaultMessage: 'Stack Rules', + } + ), +}; + +export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ + AlertConsumers.LOGS, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.APM, + AlertConsumers.UPTIME, + AlertConsumers.SLO, + 'stackAlerts', +]; + +export interface RuleFormConsumerSelectionProps { + consumers: RuleCreationValidConsumer[]; + initialConsumer?: RuleCreationValidConsumer; + onChange: (consumer: RuleCreationValidConsumer) => void; +} + +export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps) => { + const { consumers, initialConsumer, onChange } = props; + const [selectedConsumer, setSelectedConsumer] = useState(); + + const handleOnChange = useCallback( + (e: React.ChangeEvent) => { + setSelectedConsumer(e.target.value as RuleCreationValidConsumer); + onChange(e.target.value as RuleCreationValidConsumer); + }, + [setSelectedConsumer, onChange] + ); + + const formattedSelectOptions: EuiSelectOption[] = useMemo(() => { + return consumers + .reduce((result, consumer) => { + if (featureNameMap[consumer]) { + result.push({ + value: consumer, + text: featureNameMap[consumer], + }); + } + return result; + }, []) + .sort((a, b) => { + return (a.value as RuleCreationValidConsumer).localeCompare( + b.value as RuleCreationValidConsumer + ); + }); + }, [consumers]); + + // Initialize dropdown with the initial consumer, otherwise the first option + useEffect(() => { + if (selectedConsumer || formattedSelectOptions.length === 0) { + return; + } + + if ( + initialConsumer && + formattedSelectOptions.find((option) => option.value === initialConsumer) + ) { + setSelectedConsumer(initialConsumer); + onChange(initialConsumer); + return; + } + + const firstConsumer = formattedSelectOptions[0].value as RuleCreationValidConsumer; + setSelectedConsumer(firstConsumer); + onChange(firstConsumer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedConsumer, formattedSelectOptions, initialConsumer]); + + if (formattedSelectOptions.length <= 1) { + return null; + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 3bc8ef4b9f75b..f26881fa2639f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -43,6 +43,7 @@ export type { RulesListVisibleColumns, AlertSummaryTimeRange, NotifyWhenSelectOptions, + RuleCreationValidConsumer, } from './types'; export type { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index a60b5ccffdc17..54400fbe9b9d0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -26,6 +26,7 @@ import type { EuiSuperSelectOption, EuiDataGridOnColumnResizeHandler, } from '@elastic/eui'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { HttpSetup } from '@kbn/core/public'; import { KueryNode } from '@kbn/es-query'; @@ -455,6 +456,7 @@ export interface RuleAddProps> { metadata?: MetaData; ruleTypeIndex?: RuleTypeIndex; filteredRuleTypes?: string[]; + validConsumers?: RuleCreationValidConsumer[]; } export interface RuleDefinitionProps { rule: Rule; @@ -817,3 +819,11 @@ export interface NotifyWhenSelectOptions { isForEachAlertOption?: boolean; value: EuiSuperSelectOption; } + +export type RuleCreationValidConsumer = + | typeof AlertConsumers.LOGS + | typeof AlertConsumers.INFRASTRUCTURE + | typeof AlertConsumers.APM + | typeof AlertConsumers.UPTIME + | typeof AlertConsumers.SLO + | 'stackAlerts'; diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 74354c15af0ce..d5d8e696f67ab 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -395,20 +395,8 @@ export class AlertUtils { } } -export function getConsumerUnauthorizedErrorMessage( - operation: string, - alertType: string, - consumer: string -) { - return `Unauthorized to ${operation} a "${alertType}" rule for "${consumer}"`; -} - -export function getProducerUnauthorizedErrorMessage( - operation: string, - alertType: string, - producer: string -) { - return `Unauthorized to ${operation} a "${alertType}" rule by "${producer}"`; +export function getUnauthorizedErrorMessage(operation: string, alertType: string, scope: string) { + return `Unauthorized by "${scope}" to ${operation} "${alertType}" rule`; } function getDefaultAlwaysFiringAlertData( diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 2c0651f3f962a..cc7ba4fcb083f 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -8,11 +8,7 @@ export { ObjectRemover } from './object_remover'; export { getUrlPrefix } from './space_test_utils'; export { getTestRuleData } from './get_test_rule_data'; -export { - AlertUtils, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, -} from './alert_utils'; +export { AlertUtils, getUnauthorizedErrorMessage } from './alert_utils'; export type { TaskManagerDoc } from './task_manager_utils'; export { TaskManagerUtils } from './task_manager_utils'; export * from './test_assertions'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts index 5c48eb3571ec2..e636388fb269a 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -139,7 +139,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts index 400d5ad55e730..8bea4a377ea03 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -133,7 +133,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts index 65db9101c04e8..9ff58b68db98b 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts @@ -65,7 +65,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await synthtraceEsClient.clean(); await deleteDataView({ @@ -85,7 +85,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index b05d60ca7f750..0d9c1970bd8c4 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -76,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -147,7 +147,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts index 0ccff07f20828..72e1866f1d953 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts index 8b2db8b13ca05..91ae5d43bdb09 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts @@ -62,7 +62,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -83,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts index ba846751ef7b4..71b0570d21c90 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts @@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index 5f2b892a416a3..b7cf41d07e823 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -10,10 +10,9 @@ import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getTestRuleData, - getConsumerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, getUrlPrefix, ObjectRemover, - getProducerUnauthorizedErrorMessage, TaskManagerDoc, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -75,11 +74,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -181,7 +176,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -218,7 +213,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.unrestricted-noop', 'alertsFixture' @@ -228,17 +223,6 @@ export default function createAlertTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'create', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); @@ -267,7 +251,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('create', 'test.noop', 'alerts'), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alerts'), statusCode: 403, }); break; @@ -275,11 +259,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -318,7 +298,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.noop', 'some consumer patrick invented' @@ -345,11 +325,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -384,11 +360,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -481,11 +453,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.validation', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.validation', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts index 2b6086cf38d92..d6cb57261a558 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts @@ -10,8 +10,7 @@ import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, ObjectRemover, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -57,11 +56,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'delete', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('delete', 'test.noop', 'alertsFixture'), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -112,7 +107,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'delete', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -163,7 +158,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'delete', 'test.unrestricted-noop', 'alertsFixture' @@ -176,20 +171,6 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'delete', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - // Ensure task still exists - await getScheduledTask(createdAlert.scheduled_task_id); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -229,7 +210,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('delete', 'test.noop', 'alerts'), + message: getUnauthorizedErrorMessage('delete', 'test.noop', 'alerts'), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -238,11 +219,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'delete', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('delete', 'test.noop', 'alertsFixture'), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -333,11 +310,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'delete', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('delete', 'test.noop', 'alertsFixture'), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts index df9fc34e17014..8539b1a7b87c3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, TaskManagerDoc, } from '../../../../common/lib'; @@ -83,11 +82,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'disable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alertsFixture'), statusCode: 403, }); // Ensure task still exists and is still enabled @@ -158,7 +153,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'disable', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -210,7 +205,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'disable', 'test.unrestricted-noop', 'alertsFixture' @@ -220,17 +215,6 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'disable', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -273,7 +257,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('disable', 'test.noop', 'alerts'), + message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alerts'), statusCode: 403, }); break; @@ -281,11 +265,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'disable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -344,11 +324,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'disable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alertsFixture'), statusCode: 403, }); // Ensure task still exists and is still enabled diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts index 73842073a542b..8c6e3ecd5ea5a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, TaskManagerDoc, } from '../../../../common/lib'; @@ -82,11 +81,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'enable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -94,11 +89,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'enable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -168,7 +159,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'enable', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -215,7 +206,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'enable', 'test.unrestricted-noop', 'alertsFixture' @@ -225,17 +216,6 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'enable', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -268,7 +248,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('enable', 'test.noop', 'alerts'), + message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alerts'), statusCode: 403, }); break; @@ -276,11 +256,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'enable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -333,11 +309,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'enable', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index d3117ed4ce209..edacff0fbfd13 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -12,8 +12,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -50,7 +49,7 @@ const getTestUtils = ( expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -134,7 +133,7 @@ const getTestUtils = ( expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'get', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -179,7 +178,7 @@ const getTestUtils = ( expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'get', 'test.unrestricted-noop', 'alertsFixture' @@ -189,17 +188,6 @@ const getTestUtils = ( break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'get', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'global_read at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -237,11 +225,7 @@ const getTestUtils = ( expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'get', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('get', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -250,7 +234,7 @@ const getTestUtils = ( expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'get', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts index e3da329c1cbaf..65fe412e177d3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts @@ -10,8 +10,7 @@ import { getUrlPrefix, ObjectRemover, getTestRuleData, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { UserAtSpaceScenarios } from '../../../scenarios'; @@ -47,7 +46,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -87,7 +86,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'get', 'test.unrestricted-noop', 'alertsFixture' @@ -97,17 +96,6 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'get', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts index 7ad1a54de2485..9ba49b93e0b39 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts @@ -11,8 +11,7 @@ import { getUrlPrefix, ObjectRemover, getTestRuleData, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { UserAtSpaceScenarios } from '../../../scenarios'; @@ -50,7 +49,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -119,7 +118,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'get', 'test.unrestricted-noop', 'alertsFixture' @@ -129,17 +128,6 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'get', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index ecf565bd60d5d..ce55ddab37348 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -19,7 +19,7 @@ import { getTestRuleData, ObjectRemover, AlertUtils, - getConsumerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, TaskManagerUtils, getEventLog, } from '../../../../common/lib'; @@ -129,7 +129,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -253,7 +253,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -499,7 +499,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -600,7 +600,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.authorization', 'alertsFixture' @@ -718,7 +718,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -814,7 +814,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -872,7 +872,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -952,7 +952,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -1017,7 +1017,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -1073,7 +1073,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -1132,7 +1132,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -1191,7 +1191,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing', 'alertsFixture' @@ -1255,7 +1255,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing-alert-as-data', 'alertsFixture' @@ -1317,7 +1317,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing-alert-as-data', 'alertsFixture' @@ -1391,7 +1391,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing-alert-as-data', 'alertsFixture' @@ -1445,7 +1445,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing-alert-as-data', 'alertsFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts index ca3570a511d17..bba117642e3c3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -72,11 +71,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'muteAll', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('muteAll', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -137,7 +132,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAll', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -191,7 +186,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAll', 'test.unrestricted-noop', 'alertsFixture' @@ -201,17 +196,6 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'muteAll', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -257,11 +241,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'muteAll', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('muteAll', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -271,7 +251,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAll', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts index 63a285e0f4cb8..3a308f47b4812 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -72,11 +71,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'muteAlert', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('muteAlert', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -137,7 +132,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAlert', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -191,7 +186,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAlert', 'test.unrestricted-noop', 'alertsFixture' @@ -201,17 +196,6 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'muteAlert', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -257,11 +241,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'muteAlert', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('muteAlert', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -271,7 +251,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'muteAlert', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -324,11 +304,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'muteAlert', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('muteAlert', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts index 2376e05635e9c..8073ef48fbefe 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; const NOW = new Date().toISOString(); @@ -84,11 +83,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'snooze', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('snooze', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -155,7 +150,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'snooze', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -215,7 +210,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'snooze', 'test.unrestricted-noop', 'alertsFixture' @@ -225,17 +220,6 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'snooze', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -287,11 +271,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'snooze', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('snooze', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -301,7 +281,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'snooze', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -380,11 +360,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'snooze', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('snooze', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts index b4576650c58c8..9623462b8ae45 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -77,11 +76,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'unmuteAll', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('unmuteAll', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -147,7 +142,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAll', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -206,7 +201,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAll', 'test.unrestricted-noop', 'alertsFixture' @@ -216,17 +211,6 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'unmuteAll', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -277,11 +261,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'unmuteAll', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('unmuteAll', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -291,7 +271,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAll', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts index 1aa84f64a7e79..3d9f49eb3cffb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -77,11 +76,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'unmuteAlert', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('unmuteAlert', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -147,7 +142,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAlert', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -206,7 +201,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAlert', 'test.unrestricted-noop', 'alertsFixture' @@ -216,17 +211,6 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'unmuteAlert', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -277,7 +261,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAlert', 'test.restricted-noop', 'alerts' @@ -291,7 +275,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unmuteAlert', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts index 8b6a8aa2c6c45..603f8b606161b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -72,11 +71,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'unsnooze', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('unsnooze', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -138,7 +133,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unsnooze', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -193,7 +188,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unsnooze', 'test.unrestricted-noop', 'alertsFixture' @@ -203,17 +198,6 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'unsnooze', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -260,11 +244,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'unsnooze', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('unsnooze', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -274,7 +254,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'unsnooze', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index b6972730cc780..31250862265ea 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -14,8 +14,7 @@ import { getTestRuleData, ObjectRemover, ensureDatetimeIsWithinRange, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -89,11 +88,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -199,7 +194,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'update', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -289,7 +284,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'update', 'test.unrestricted-noop', 'alertsFixture' @@ -299,17 +294,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'update', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); @@ -391,11 +375,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.restricted-noop', - 'alerts' - ), + message: getUnauthorizedErrorMessage('update', 'test.restricted-noop', 'alerts'), statusCode: 403, }); break; @@ -405,7 +385,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'update', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -504,11 +484,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -591,11 +567,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -763,11 +735,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.validation', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.validation', 'alertsFixture'), statusCode: 403, }); break; @@ -864,11 +832,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -941,11 +905,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.longRunning', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.longRunning', 'alertsFixture'), statusCode: 403, }); break; @@ -1009,11 +969,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'update', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts index 6a594316796d4..7e2f4e74aa023 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts @@ -14,8 +14,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -71,11 +70,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'updateApiKey', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -136,7 +131,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'updateApiKey', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -190,7 +185,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'updateApiKey', 'test.unrestricted-noop', 'alertsFixture' @@ -200,17 +195,6 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'updateApiKey', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); @@ -256,7 +240,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'updateApiKey', 'test.restricted-noop', 'alerts' @@ -270,7 +254,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'updateApiKey', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -334,11 +318,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'updateApiKey', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts index 4febd9b866f15..ef50420bb6561 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts @@ -8,7 +8,12 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios, SuperuserAtSpace1 } from '../../../scenarios'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getUnauthorizedErrorMessage, +} from '../../../../common/lib'; const getDefaultRules = (response: any) => ({ id: response.body.rules[0].id, @@ -130,7 +135,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDelete a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDelete', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -195,8 +200,11 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: - 'Unauthorized to bulkDelete a "test.restricted-noop" rule for "alertsRestrictedFixture"', + message: getUnauthorizedErrorMessage( + 'bulkDelete', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -342,7 +350,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDelete a "test.noop" rule by "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDelete', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -409,7 +417,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDelete a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDelete', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -481,7 +489,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDelete a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDelete', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -550,7 +558,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDelete a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDelete', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts index 13a89de8381ca..8ea0b913620aa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts @@ -8,7 +8,12 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios, SuperuserAtSpace1 } from '../../../scenarios'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getUnauthorizedErrorMessage, +} from '../../../../common/lib'; const getDefaultRules = (response: any) => ({ id: response.body.rules[0].id, @@ -95,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDisable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDisable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -112,7 +117,7 @@ export default ({ getService }: FtrProviderContext) => { } }); - it('should handle bulk disable of one rule appropriately based on id when consumer is the same as producer', async () => { + it('should handle bulk disable of one rule appropriately based on id when consumer is the same as producer', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -144,8 +149,11 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: - 'Unauthorized to bulkDisable a "test.restricted-noop" rule for "alertsRestrictedFixture"', + message: getUnauthorizedErrorMessage( + 'bulkDisable', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -269,7 +277,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDisable a "test.noop" rule by "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDisable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -328,7 +336,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDisable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDisable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -378,7 +386,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDisable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDisable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -414,7 +422,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkDisable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkDisable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts index 2f3f36b9dc34b..c948791e5ea49 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts @@ -13,8 +13,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, - getProducerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -85,7 +84,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEdit a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEdit', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -178,7 +177,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEdit a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEdit', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -249,7 +248,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'bulkEdit', 'test.restricted-noop', 'alertsRestrictedFixture' @@ -310,7 +309,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'bulkEdit', 'test.unrestricted-noop', 'alertsFixture' @@ -321,17 +320,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.body).to.eql({ - error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( - 'bulkEdit', - 'test.unrestricted-noop', - 'alertsRestrictedFixture' - ), - statusCode: 403, - }); - expect(response.statusCode).to.eql(403); - break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.body.rules[0].tags).to.eql(['foo', 'tag-A', 'tag-B']); @@ -390,7 +378,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: getProducerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'bulkEdit', 'test.restricted-noop', 'alertsRestrictedFixture' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts index 3f1239558c5eb..471faeda4c25b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts @@ -8,7 +8,12 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios, SuperuserAtSpace1 } from '../../../scenarios'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getUnauthorizedErrorMessage, +} from '../../../../common/lib'; const defaultSuccessfulResponse = { total: 1, rules: [], errors: [], taskIdsFailedToBeEnabled: [] }; @@ -61,7 +66,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEnable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEnable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -147,8 +152,11 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: - 'Unauthorized to bulkEnable a "test.restricted-noop" rule for "alertsRestrictedFixture"', + message: getUnauthorizedErrorMessage( + 'bulkEnable', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -253,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEnable a "test.noop" rule by "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEnable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -303,7 +311,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEnable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEnable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -353,7 +361,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEnable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEnable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); @@ -393,7 +401,7 @@ export default ({ getService }: FtrProviderContext) => { case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', - message: 'Unauthorized to bulkEnable a "test.noop" rule for "alertsFixture"', + message: getUnauthorizedErrorMessage('bulkEnable', 'test.noop', 'alertsFixture'), statusCode: 403, }); expect(response.statusCode).to.eql(403); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts index 46aef808c0877..2cbec3853f02e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts @@ -10,7 +10,7 @@ import { Spaces, UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getTestRuleData, - getConsumerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, getUrlPrefix, ObjectRemover, TaskManagerDoc, @@ -121,11 +121,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/run_soon.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/run_soon.ts index 831edffbae51a..461d603734d10 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/run_soon.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/run_soon.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../../scenarios'; import { getTestRuleData, - getConsumerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, getUrlPrefix, ObjectRemover, } from '../../../../common/lib'; @@ -50,11 +50,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'runSoon', - 'test.noop', - 'alertsFixture' - ), + message: getUnauthorizedErrorMessage('runSoon', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index f3e7d8891cde3..eb9f90cb41f2a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -15,7 +15,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover, - getConsumerUnauthorizedErrorMessage, + getUnauthorizedErrorMessage, TaskManagerDoc, } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -444,7 +444,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { expect(response.status).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.noop', 'some consumer patrick invented' diff --git a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts index 4883f01b5ac6e..d67cd7051860c 100644 --- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts +++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts @@ -36,8 +36,8 @@ export default function ({ getService }: FtrProviderContext) { return fieldStat.name === 'geo_point'; } ); - expect(geoPointFieldStats.count).to.be(31); - expect(geoPointFieldStats.index_count).to.be(9); + expect(geoPointFieldStats.count).to.be(39); + expect(geoPointFieldStats.index_count).to.be(10); const geoShapeFieldStats = apiResponse.cluster_stats.indices.mappings.field_types.find( (fieldStat: estypes.ClusterStatsFieldTypes) => { diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index 42c4a0bd6d1c3..180e2826ba120 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -8,13 +8,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default ({ getService }: FtrProviderContext) => { +export default ({ getService, getPageObjects }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); const RULE_ENDPOINT = '/api/alerting/rule'; + const INTERNAL_RULE_ENDPOINT = '/internal/alerting/rules'; + + const PageObjects = getPageObjects(['header']); async function createRule(rule: any): Promise { const ruleResponse = await supertest.post(RULE_ENDPOINT).set('kbn-xsrf', 'foo').send(rule); @@ -22,11 +25,21 @@ export default ({ getService }: FtrProviderContext) => { return ruleResponse.body.id; } + async function getRuleByName(name: string) { + const { + body: { data: rules }, + } = await supertest + .get(`${INTERNAL_RULE_ENDPOINT}/_find?search=${name}&search_fields=name`) + .expect(200); + return rules.find((rule: any) => rule.name === name); + } + async function deleteRuleById(ruleId: string) { - const ruleResponse = await supertest - .delete(`${RULE_ENDPOINT}/${ruleId}`) - .set('kbn-xsrf', 'foo'); - expect(ruleResponse.status).to.eql(204); + await supertest + .patch(`${INTERNAL_RULE_ENDPOINT}/_bulk_delete`) + .set('kbn-xsrf', 'foo') + .send({ ids: [ruleId] }) + .expect(200); return true; } @@ -42,11 +55,47 @@ export default ({ getService }: FtrProviderContext) => { return rows; }; + const selectAndFillInEsQueryRule = async (ruleName: string) => { + await testSubjects.setValue('ruleNameInput', ruleName); + await testSubjects.click(`.es-query-SelectOption`); + await testSubjects.click('queryFormType_esQuery'); + await testSubjects.click('selectIndexExpression'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('*'); + const filterSelectItems = await find.allByCssSelector(`.euiFilterSelectItem`); + await filterSelectItems[1].click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + }; + describe('Observability Rules page', function () { this.tags('includeFirefox'); const observability = getService('observability'); + const navigateAndOpenCreateRuleFlyout = async () => { + await observability.alerts.common.navigateToRulesPage(); + await retry.waitFor( + 'Create Rule button is visible', + async () => await testSubjects.exists('createRuleButton') + ); + await retry.waitFor( + 'Create Rule button is enabled', + async () => await testSubjects.isEnabled('createRuleButton') + ); + await observability.alerts.rulesPage.clickCreateRuleButton(); + await retry.waitFor( + 'Create Rule flyout is visible', + async () => await testSubjects.exists('addRuleFlyoutTitle') + ); + }; + before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); @@ -67,20 +116,81 @@ export default ({ getService }: FtrProviderContext) => { describe('Create rule button', () => { it('Show Create Rule flyout when Create Rule button is clicked', async () => { - await observability.alerts.common.navigateToRulesPage(); - await retry.waitFor( - 'Create Rule button is visible', - async () => await testSubjects.exists('createRuleButton') + await navigateAndOpenCreateRuleFlyout(); + }); + }); + + describe('Create rules flyout', async () => { + const ruleName = 'esQueryRule'; + + afterEach(async () => { + const rule = await getRuleByName(ruleName); + if (rule) { + await deleteRuleById(rule.id); + } + await observability.users.restoreDefaultTestUserRole(); + }); + + it('Allows ES query rules to be created by users with only infrastructure feature enabled', async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + infrastructure: ['all'], + }) ); - await retry.waitFor( - 'Create Rule button is enabled', - async () => await testSubjects.isEnabled('createRuleButton') + await navigateAndOpenCreateRuleFlyout(); + await selectAndFillInEsQueryRule(ruleName); + + await testSubjects.click('saveRuleButton'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + + const tableRows = await find.allByCssSelector('.euiTableRow'); + const rows = await getRulesList(tableRows); + expect(rows.length).to.be(1); + expect(rows[0].name).to.contain(ruleName); + }); + + it('allows ES query rules to be created by users with only logs feature enabled', async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + logs: ['all'], + }) ); - await observability.alerts.rulesPage.clickCreateRuleButton(); - await retry.waitFor( - 'Create Rule flyout is visible', - async () => await testSubjects.exists('addRuleFlyoutTitle') + await navigateAndOpenCreateRuleFlyout(); + await selectAndFillInEsQueryRule(ruleName); + + await testSubjects.click('saveRuleButton'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + + const tableRows = await find.allByCssSelector('.euiTableRow'); + const rows = await getRulesList(tableRows); + expect(rows.length).to.be(1); + expect(rows[0].name).to.contain(ruleName); + }); + + it('Should allow the user to select consumers when creating ES query rules', async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + logs: ['all'], + infrastructure: ['all'], + }) ); + + await navigateAndOpenCreateRuleFlyout(); + await selectAndFillInEsQueryRule(ruleName); + + await retry.waitFor('consumer select modal is visible', async () => { + return await testSubjects.exists('ruleFormConsumerSelect'); + }); + + const consumerSelect = await testSubjects.find('ruleFormConsumerSelect'); + const consumerOptions = await consumerSelect.findAllByTagName('option'); + + // There seems to be an extra option, so assert options + 1 + expect(consumerOptions.length).eql(3); + expect(await consumerOptions[1].getAttribute('value')).eql('infrastructure'); + expect(await consumerOptions[2].getAttribute('value')).eql('logs'); }); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 708eb4a5c3abc..eb232ce45c8e3 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -96,6 +96,7 @@ export default function ({ getService }: FtrProviderContext) { 'alerting:monitoring_alert_thread_pool_write_rejections', 'alerting:monitoring_ccr_read_exceptions', 'alerting:monitoring_shard_size', + 'alerting:observability.rules.threshold', 'alerting:siem.eqlRule', 'alerting:siem.indicatorRule', 'alerting:siem.mlRule', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts index eaf12f3c69e5f..7ff524378eb34 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts @@ -54,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -142,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts index c101af3bc9aa8..481a669113af6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -135,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index a963245651d7a..034e8ce423fec 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -81,7 +81,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -150,7 +150,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts index eacdf7b34fa4f..5aa5433bedbf3 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts @@ -54,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -140,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts index a78a008f93e61..a1e9b789c037a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { }); await esClient.deleteByQuery({ index: '.kibana-event-log-*', - query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, + query: { term: { 'kibana.alert.rule.consumer': 'logs' } }, }); await deleteDataView({ supertest, @@ -84,7 +84,7 @@ export default function ({ getService }: FtrProviderContext) { const createdRule = await createRule({ supertest, tags: ['observability'], - consumer: 'alerts', + consumer: 'logs', name: 'Threshold rule', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, params: { @@ -158,7 +158,7 @@ export default function ({ getService }: FtrProviderContext) { 'kibana.alert.rule.category', 'Threshold (Technical Preview)' ); - expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'alerts'); + expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'logs'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability'); expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0); From 7337d16145efddb9145d5cf83a9c8a412cccb0dc Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Thu, 7 Sep 2023 18:39:31 -0400 Subject: [PATCH 02/15] [RAM] Update RBAC to manage es-query/generic o11y threshold rule (#165182) ## Summary This PR is here to update the alerting authorization model to allow `.es-query` and `observability.rules.threshold` to work with different consumers. We also the rule find's API to allow to filter on consumers. We update the alert client from the rule_registry plugin to get the alert index through the rule type and the alerting plugin like we did for the search strategy. ### 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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-rule-data-utils/index.ts | 1 + .../src/rule_types/index.ts | 10 + .../src/rule_types/o11y_rules.ts | 9 + .../src/rule_types/stack_rules.ts | 5 +- .../top_nav/open_alerts_popover.tsx | 3 +- src/plugins/discover/tsconfig.json | 3 +- x-pack/plugins/alerting/common/rule.ts | 1 + .../alerts_client/alerts_client.test.ts | 1 + .../legacy_alerts_client.test.ts | 1 + .../alerts_client/lib/format_rule.test.ts | 1 + .../alerts_service/alerts_service.test.ts | 1 + .../methods/bulk_edit/bulk_edit_rules.test.ts | 4 + .../rule/methods/create/create_rule.test.ts | 12 + .../alerting_authorization.test.ts | 663 +++++++++++++++++- .../authorization/alerting_authorization.ts | 86 ++- .../alerting_authorization_kuery.test.ts | 16 + .../alerting_event_logger.test.ts | 1 + ...eate_alert_event_log_record_object.test.ts | 1 + .../server/routes/aggregate_rules.test.ts | 1 + .../alerting/server/routes/aggregate_rules.ts | 3 + .../alerting/server/routes/find_rules.test.ts | 1 + .../alerting/server/routes/find_rules.ts | 3 + .../alerting/server/routes/health.test.ts | 1 + .../server/routes/legacy/health.test.ts | 1 + .../routes/legacy/list_alert_types.test.ts | 4 + .../alerting/server/routes/rule_types.test.ts | 7 +- .../alerting/server/routes/rule_types.ts | 7 +- .../server/rule_type_registry.test.ts | 70 +- .../alerting/server/rule_type_registry.ts | 6 + ...type_registry_deprecated_consumers.test.ts | 103 +++ ...rule_type_registry_deprecated_consumers.ts | 88 +++ .../migrate_legacy_actions.test.ts | 1 + .../rules_client/lib/validate_actions.test.ts | 1 + .../server/rules_client/methods/aggregate.ts | 6 +- .../server/rules_client/methods/find.ts | 10 +- .../rules_client/tests/aggregate.test.ts | 2 + .../rules_client/tests/bulk_delete.test.ts | 1 + .../server/rules_client/tests/find.test.ts | 8 + .../server/rules_client/tests/get.test.ts | 2 + .../rules_client/tests/get_tags.test.ts | 2 + .../alerting/server/rules_client/tests/lib.ts | 1 + .../tests/list_rule_types.test.ts | 5 + .../server/rules_client/tests/resolve.test.ts | 2 + .../server/rules_client/tests/update.test.ts | 4 + .../rules_client_conflict_retries.test.ts | 2 + .../task_runner/execution_handler.test.ts | 1 + .../alerting/server/task_runner/fixtures.ts | 1 + .../task_runner/task_runner_factory.test.ts | 1 + x-pack/plugins/apm/server/feature.ts | 7 +- x-pack/plugins/apm/tsconfig.json | 1 - .../__snapshots__/oss_features.test.ts.snap | 40 +- .../plugins/features/server/oss_features.ts | 17 - x-pack/plugins/infra/server/features.ts | 4 +- x-pack/plugins/infra/tsconfig.json | 1 - .../plugins/observability/common/constants.ts | 3 - .../threshold/components/alert_flyout.tsx | 7 +- .../hooks/use_get_filtered_rule_types.ts | 2 +- .../public/pages/alerts/alerts.tsx | 7 +- .../pages/alerts/components/alert_actions.tsx | 4 +- .../public/pages/rules/rules.tsx | 1 + .../public/pages/rules/rules_tab.tsx | 2 + .../register_observability_rule_types.ts | 7 +- .../threshold/register_threshold_rule_type.ts | 2 +- x-pack/plugins/observability/server/plugin.ts | 7 +- .../server/alert_data_client/alerts_client.ts | 62 +- .../alerts_client_factory.test.ts | 2 + .../alerts_client_factory.ts | 9 +- .../tests/bulk_update.test.ts | 17 +- .../tests/bulk_update_cases.test.ts | 1 + .../tests/find_alerts.test.ts | 13 +- .../alert_data_client/tests/get.test.ts | 13 +- .../tests/remove_cases_from_alerts.test.ts | 2 + .../alert_data_client/tests/update.test.ts | 13 +- .../server/lib/get_authz_filter.ts | 6 +- x-pack/plugins/rule_registry/server/plugin.ts | 1 + .../get_browser_fields_by_feature_id.ts | 8 +- .../server/search_strategy/search_strategy.ts | 12 +- .../server/routes/alerts_client_mock.test.ts | 16 + x-pack/plugins/stack_alerts/common/index.ts | 1 - .../search_source_expression_form.tsx | 2 +- .../geo_containment/query_builder/index.tsx | 2 +- x-pack/plugins/stack_alerts/server/feature.ts | 4 +- .../server/rule_types/es_query/constants.ts | 1 - .../rule_types/es_query/lib/fetch_es_query.ts | 2 +- .../server/rule_types/es_query/rule_type.ts | 5 +- .../rule_types/geo_containment/rule_type.ts | 2 +- .../rule_types/index_threshold/rule_type.ts | 8 +- x-pack/plugins/synthetics/server/feature.ts | 4 +- x-pack/plugins/synthetics/tsconfig.json | 1 - .../public/application/constants/index.ts | 9 +- .../hooks/use_load_rule_aggregations_query.ts | 4 +- .../application/hooks/use_load_rules_query.ts | 5 +- .../application/lib/rule_api/aggregate.ts | 2 + .../lib/rule_api/aggregate_helpers.ts | 1 + .../lib/rule_api/aggregate_kuery_filter.ts | 2 + .../application/lib/rule_api/rules_helpers.ts | 1 + .../lib/rule_api/rules_kuery_filter.ts | 2 + .../sections/rule_form/rule_add.tsx | 3 +- .../sections/rule_form/rule_form.tsx | 9 + .../rules_list/components/rules_list.tsx | 4 + .../triggers_actions_ui/public/types.ts | 5 +- .../threshold_rule/avg_pct_fired.ts | 2 +- .../threshold_rule/avg_pct_no_data.ts | 2 +- .../threshold_rule/avg_us_fired.ts | 2 +- .../custom_eq_avg_bytes_fired.ts | 2 +- .../threshold_rule/documents_count_fired.ts | 2 +- .../threshold_rule/group_by_fired.ts | 2 +- .../observability/threshold_rule_data_view.ts | 2 +- .../group2/tests/alerting/rbac_legacy.ts | 2 +- .../group3/tests/alerting/bulk_enable.ts | 1 - .../tests/alerting/group1/rule_types.ts | 1 + .../rule_registry/o11y_alerts/data.json | 104 +++ .../rule_registry/o11y_alerts/mappings.json | 66 ++ .../tests/basic/get_alert_summary.ts | 4 +- .../tests/basic/get_alerts_index.ts | 6 +- .../basic/get_browser_fields_by_feature_id.ts | 3 +- .../threshold_rule/avg_pct_fired.ts | 2 +- .../threshold_rule/avg_pct_no_data.ts | 2 +- .../custom_eq_avg_bytes_fired.ts | 2 +- .../threshold_rule/documents_count_fired.ts | 2 +- .../threshold_rule/group_by_fired.ts | 2 +- x-pack/test_serverless/tsconfig.json | 1 + 122 files changed, 1454 insertions(+), 288 deletions(-) create mode 100644 packages/kbn-rule-data-utils/src/rule_types/index.ts create mode 100644 packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts rename x-pack/plugins/stack_alerts/common/constants.ts => packages/kbn-rule-data-utils/src/rule_types/stack_rules.ts (57%) create mode 100644 x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.test.ts create mode 100644 x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.ts create mode 100644 x-pack/test/functional/es_archives/rule_registry/o11y_alerts/data.json create mode 100644 x-pack/test/functional/es_archives/rule_registry/o11y_alerts/mappings.json diff --git a/packages/kbn-rule-data-utils/index.ts b/packages/kbn-rule-data-utils/index.ts index ea0028b972ed9..3e9787891be05 100644 --- a/packages/kbn-rule-data-utils/index.ts +++ b/packages/kbn-rule-data-utils/index.ts @@ -14,3 +14,4 @@ export * from './src/alerts_as_data_severity'; export * from './src/alerts_as_data_status'; export * from './src/alerts_as_data_cases'; export * from './src/routes/stack_rule_paths'; +export * from './src/rule_types'; diff --git a/packages/kbn-rule-data-utils/src/rule_types/index.ts b/packages/kbn-rule-data-utils/src/rule_types/index.ts new file mode 100644 index 0000000000000..b2f860b063c9b --- /dev/null +++ b/packages/kbn-rule-data-utils/src/rule_types/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './stack_rules'; +export * from './o11y_rules'; diff --git a/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts b/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts new file mode 100644 index 0000000000000..b004d1758d4c7 --- /dev/null +++ b/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; diff --git a/x-pack/plugins/stack_alerts/common/constants.ts b/packages/kbn-rule-data-utils/src/rule_types/stack_rules.ts similarity index 57% rename from x-pack/plugins/stack_alerts/common/constants.ts rename to packages/kbn-rule-data-utils/src/rule_types/stack_rules.ts index 38b0a2c6e73e3..ff426a9069537 100644 --- a/x-pack/plugins/stack_alerts/common/constants.ts +++ b/packages/kbn-rule-data-utils/src/rule_types/stack_rules.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export const STACK_ALERTS_FEATURE_ID = 'stackAlerts'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 75202710945dd..0a81a70ecd519 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { DataView } from '@kbn/data-plugin/common'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { DiscoverStateContainer } from '../../services/discover_state'; import { DiscoverServices } from '../../../../build_services'; @@ -84,7 +85,7 @@ export function AlertsPopover({ return triggersActionsUi?.getAddRuleFlyout({ metadata: discoverMetadata, - consumer: 'discover', + consumer: STACK_ALERTS_FEATURE_ID, onClose: (_, metadata) => { onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData); onClose(); diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 3328073a026e7..acd0edd107100 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -71,7 +71,8 @@ "@kbn/react-kibana-mount", "@kbn/react-kibana-context-render", "@kbn/unified-data-table", - "@kbn/no-data-page-plugin" + "@kbn/no-data-page-plugin", + "@kbn/rule-data-utils" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 57ef90ed99620..22f7ae2dfc279 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -126,6 +126,7 @@ export interface AggregateOptions { id: string; }; filter?: string | KueryNode; + filterConsumers?: string[]; page?: number; perPage?: number; } diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts index 78b2e41431c22..f09e1a10d019f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts @@ -62,6 +62,7 @@ const ruleType: jest.Mocked = { mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, shouldWrite: true, }, + validLegacyConsumers: [], }; const mockLegacyAlertsClient = legacyAlertsClientMock.create(); diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index f8c341e132e51..6a884159f6f7c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -99,6 +99,7 @@ const ruleType: jest.Mocked = { validate: { params: schema.any(), }, + validLegacyConsumers: [], }; const testAlert1 = { diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts index cb90b75d2c16d..6e8d28de01753 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts @@ -29,6 +29,7 @@ const ruleType: jest.Mocked = { mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, shouldWrite: true, }, + validLegacyConsumers: [], }; describe('formatRule', () => { diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index 90552e1d5b0ac..954a03d697a61 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -202,6 +202,7 @@ const ruleType: jest.Mocked = { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }; const ruleTypeWithAlertDefinition: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index 5e672bcc83c68..f92197e76bcca 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -231,6 +231,7 @@ describe('bulkEdit()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }); (migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock); @@ -733,6 +734,7 @@ describe('bulkEdit()', () => { mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, shouldWrite: true, }, + validLegacyConsumers: [], }); const existingAction = { frequency: { @@ -2339,6 +2341,7 @@ describe('bulkEdit()', () => { return { state: {} }; }, producer: 'alerts', + validLegacyConsumers: [], }); const result = await rulesClient.bulkEdit({ @@ -2383,6 +2386,7 @@ describe('bulkEdit()', () => { return { state: {} }; }, producer: 'alerts', + validLegacyConsumers: [], }); const result = await rulesClient.bulkEdit({ diff --git a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts index 496cfe317586f..26d343f1480c6 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts @@ -1540,6 +1540,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ params: ruleParams, @@ -1727,6 +1728,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ params: ruleParams, @@ -2546,6 +2548,7 @@ describe('create()', () => { return { state: {} }; }, producer: 'alerts', + validLegacyConsumers: [], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"params invalid: [param1]: expected value of type [string] but got [undefined]"` @@ -3020,6 +3023,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const createdAttributes = { ...data, @@ -3092,6 +3096,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ schedule: { interval: '1s' } }); @@ -3129,6 +3134,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3221,6 +3227,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3270,6 +3277,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3332,6 +3340,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3412,6 +3421,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3616,6 +3626,7 @@ describe('create()', () => { mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, shouldWrite: true, }, + validLegacyConsumers: [], })); const data = getMockData({ @@ -3668,6 +3679,7 @@ describe('create()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const data = getMockData({ diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 12970b7e91cfb..c68532082f18a 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -22,7 +22,7 @@ import { } from './alerting_authorization'; import { v4 as uuidv4 } from 'uuid'; import { RecoveredActionGroup } from '../../common'; -import { RegistryRuleType } from '../rule_type_registry'; +import { NormalizedRuleType, RegistryRuleType } from '../rule_type_registry'; import { AlertingAuthorizationFilterType } from './alerting_authorization_kuery'; import { schema } from '@kbn/config-schema'; @@ -201,6 +201,7 @@ beforeEach(() => { validate: { params: schema.any(), }, + validLegacyConsumers: [], })); features.getKibanaFeatures.mockReturnValue([ myAppFeature, @@ -764,6 +765,7 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const myAppAlertType: RegistryRuleType = { actionGroups: [], @@ -778,6 +780,7 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const mySecondAppAlertType: RegistryRuleType = { actionGroups: [], @@ -792,6 +795,7 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); test('omits filter when there is no authorization api', async () => { @@ -1165,6 +1169,7 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const myAppAlertType: RegistryRuleType = { actionGroups: [], @@ -1179,6 +1184,7 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); beforeEach(() => { @@ -1245,6 +1251,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, Object { "actionGroups": Array [], @@ -1280,6 +1287,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, } `); @@ -1361,15 +1369,12 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, Object { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -1392,6 +1397,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, } `); @@ -1442,10 +1448,6 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -1464,6 +1466,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, } `); @@ -1548,10 +1551,6 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -1574,15 +1573,12 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, Object { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, - }, "myApp": Object { "all": false, "read": true, @@ -1605,6 +1601,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, } `); @@ -1668,10 +1665,6 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -1694,6 +1687,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, } `); @@ -1714,6 +1708,7 @@ describe('AlertingAuthorization', () => { isExportable: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const myAppAlertType: RegistryRuleType = { actionGroups: [], @@ -1728,6 +1723,7 @@ describe('AlertingAuthorization', () => { isExportable: true, hasAlertsMappings: true, hasFieldsForAAD: true, + validLegacyConsumers: [], }; const mySecondAppAlertType: RegistryRuleType = { actionGroups: [], @@ -1742,6 +1738,7 @@ describe('AlertingAuthorization', () => { isExportable: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); beforeEach(() => { @@ -1806,6 +1803,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, }, "hasAllRequested": false, @@ -1881,6 +1879,7 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, }, "hasAllRequested": false, @@ -1889,4 +1888,626 @@ describe('AlertingAuthorization', () => { `); }); }); + + describe('8.11+', () => { + let alertAuthorization: AlertingAuthorization; + + const setOfRuleTypes: RegistryRuleType[] = [ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: '.esQuery', + name: 'ES Query', + producer: 'stackAlerts', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: ['discover', 'alerts'], + }, + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: '.threshold-rule-o11y', + name: 'New threshold 011y', + producer: 'observability', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: [], + }, + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: '.infrastructure-threshold-o11y', + name: 'Metrics o11y', + producer: 'infrastructure', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: ['alerts'], + }, + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: '.logs-threshold-o11y', + name: 'Logs o11y', + producer: 'logs', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: ['alerts'], + }, + ]; + + const onlyStackAlertsKibanaPrivileges = [ + { + privilege: mockAuthorizationAction('.esQuery', 'stackAlerts', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.esQuery', 'stackAlerts', 'rule', 'find'), + authorized: true, + }, + ]; + const only011yKibanaPrivileges = [ + { + privilege: mockAuthorizationAction( + '.infrastructure-threshold-o11y', + 'infrastructure', + 'rule', + 'create' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + '.infrastructure-threshold-o11y', + 'infrastructure', + 'rule', + 'find' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + '.threshold-rule-o11y', + 'infrastructure', + 'rule', + 'create' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + '.threshold-rule-o11y', + 'infrastructure', + 'rule', + 'find' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.logs-threshold-o11y', 'logs', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.logs-threshold-o11y', 'logs', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.threshold-rule-o11y', 'logs', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.threshold-rule-o11y', 'logs', 'rule', 'find'), + authorized: true, + }, + ]; + const onlyLogsAndStackAlertsKibanaPrivileges = [ + { + privilege: mockAuthorizationAction('.esQuery', 'stackAlerts', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.esQuery', 'stackAlerts', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.logs-threshold-o11y', 'logs', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.logs-threshold-o11y', 'logs', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.threshold-rule-o11y', 'logs', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('.threshold-rule-o11y', 'logs', 'rule', 'find'), + authorized: true, + }, + ]; + + beforeEach(async () => { + ruleTypeRegistry.list.mockReturnValue(new Set(setOfRuleTypes)); + ruleTypeRegistry.get.mockImplementation((id: string) => { + if (setOfRuleTypes.some((rt) => rt.id === id)) { + const ruleType = setOfRuleTypes.find((rt) => rt.id === id); + return (ruleType ?? {}) as NormalizedRuleType<{}, {}, {}, {}, {}, '', '', {}>; + } + return {} as NormalizedRuleType<{}, {}, {}, {}, {}, '', '', {}>; + }); + }); + + describe('user only access to stack alerts + discover', () => { + beforeEach(() => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.mode.useRbacForRequest.mockReturnValue(true); + + features.getKibanaFeatures.mockReset(); + features.getKibanaFeatures.mockReturnValue([ + mockFeature('stackAlerts', ['.esQuery']), + mockFeature('discover', []), + ]); + checkPrivileges.mockReset(); + checkPrivileges.mockResolvedValue({ + username: 'onlyStack', + hasAllRequested: true, + privileges: { + kibana: onlyStackAlertsKibanaPrivileges, + }, + }); + authorization.checkPrivilegesDynamicallyWithRequest.mockReset(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + getSpace, + getSpaceId, + }); + }); + + describe('ensureAuthorized', () => { + test('should allow to create .esquery rule type with stackAlerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'stackAlerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with discover consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'discover', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .esquery rule type with logs consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'logs', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"logs\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .esquery rule type with infrastructure consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'infrastructure', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"infrastructure\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .threshold-rule-o11y rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.threshold-rule-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"alerts\\" to create \\".threshold-rule-o11y\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .logs-threshold-o11y rule type with alerts infrastructure', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.logs-threshold-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"alerts\\" to create \\".logs-threshold-o11y\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + }); + test('creates a filter based on the privileged types', async () => { + expect( + ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).filter + ).toEqual( + fromKueryExpression( + `path.to.rule_type_id:.esQuery and consumer-field:(alerts or stackAlerts or discover)` + ) + ); + }); + }); + + describe('user only access to o11y', () => { + beforeEach(() => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.mode.useRbacForRequest.mockReturnValue(true); + + features.getKibanaFeatures.mockReset(); + features.getKibanaFeatures.mockReturnValue([ + mockFeature('infrastructure', [ + '.infrastructure-threshold-o11y', + '.threshold-rule-o11y', + '.esQuery', + ]), + mockFeature('logs', ['.threshold-rule-o11y', '.esQuery', '.logs-threshold-o11y']), + ]); + checkPrivileges.mockReset(); + checkPrivileges.mockResolvedValue({ + username: 'onlyO11y', + hasAllRequested: true, + privileges: { + kibana: only011yKibanaPrivileges, + }, + }); + authorization.checkPrivilegesDynamicallyWithRequest.mockReset(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + getSpace, + getSpaceId, + }); + }); + + describe('ensureAuthorized', () => { + test('should throw an error to create .esquery rule type with stackAlerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'stackAlerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"stackAlerts\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .esquery rule type with discover consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'discover', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"discover\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .threshold-rule-o11y rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.threshold-rule-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"alerts\\" to create \\".threshold-rule-o11y\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with logs consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'logs', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with logs infrastructure', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'infrastructure', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .logs-threshold-o11y rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.logs-threshold-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .threshold-rule-o11y rule type with logs consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.threshold-rule-o11y', + consumer: 'logs', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + }); + test('creates a filter based on the privileged types', async () => { + expect( + ( + await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }, + new Set(['infrastructure', 'logs']) + ) + ).filter + ).toEqual( + fromKueryExpression( + `(path.to.rule_type_id:.infrastructure-threshold-o11y and consumer-field:(infrastructure or alerts)) or (path.to.rule_type_id:.threshold-rule-o11y and consumer-field:(infrastructure or logs)) or (path.to.rule_type_id:.logs-threshold-o11y and consumer-field:(logs or alerts))` + ) + ); + }); + }); + + describe('user only access to logs and stackAlerts', () => { + beforeEach(() => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.mode.useRbacForRequest.mockReturnValue(true); + + features.getKibanaFeatures.mockClear(); + features.getKibanaFeatures.mockReturnValue([ + mockFeature('stackAlerts', ['.esQuery']), + mockFeature('logs', ['.logs-threshold-o11y', '.threshold-rule-o11y', '.esQuery']), + ]); + checkPrivileges.mockClear(); + checkPrivileges.mockResolvedValue({ + username: 'stackAndLogs', + hasAllRequested: true, + privileges: { + kibana: onlyLogsAndStackAlertsKibanaPrivileges, + }, + }); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + getSpace, + getSpaceId, + }); + }); + + describe('ensureAuthorized', () => { + test('should allow to create .esquery rule type with stackAlerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'stackAlerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with discover consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'discover', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .esquery rule type with logs consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'logs', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should allow to create .logs-threshold-o11y rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.logs-threshold-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .threshold-rule-o11y rule type with logs consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.threshold-rule-o11y', + consumer: 'logs', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).resolves.toEqual(undefined); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .esquery rule type with logs infrastructure', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'infrastructure', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"infrastructure\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .threshold-rule-o11y rule type with alerts consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.threshold-rule-o11y', + consumer: 'alerts', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"alerts\\" to create \\".threshold-rule-o11y\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + test('should throw an error to create .esquery rule type with infrastructure consumer', async () => { + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: '.esQuery', + consumer: 'infrastructure', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"infrastructure\\" to create \\".esQuery\\" rule"` + ); + + expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + }); + }); + test('creates a filter based on the privileged types', async () => { + expect( + ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).filter + ).toEqual( + fromKueryExpression( + `(path.to.rule_type_id:.esQuery and consumer-field:(alerts or stackAlerts or logs or discover)) or (path.to.rule_type_id:.logs-threshold-o11y and consumer-field:(alerts or stackAlerts or logs or discover)) or (path.to.rule_type_id:.threshold-rule-o11y and consumer-field:(alerts or stackAlerts or logs or discover))` + ) + ); + }); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 01f035070d63f..90f4b189b1197 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { fromPairs, has } from 'lodash'; +import { has, isEmpty } from 'lodash'; import { KibanaRequest } from '@kbn/core/server'; import { JsonObject } from '@kbn/utility-types'; import { KueryNode } from '@kbn/es-query'; @@ -175,9 +175,11 @@ export class AlertingAuthorization { }: EnsureAuthorizedOpts) { const { authorization } = this; const ruleType = this.ruleTypeRegistry.get(ruleTypeId); - // We have some rules with consumer of "alerts" which indirectly means the same as - // a consumer of the rule type producer. Let's simplify the code and set it accordingly - const consumer = legacyConsumer === ALERTS_FEATURE_ID ? ruleType.producer : legacyConsumer; + const consumer = getValidConsumer({ + validLegacyConsumers: ruleType.validLegacyConsumers, + legacyConsumer, + producer: ruleType.producer, + }); const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { @@ -208,12 +210,18 @@ export class AlertingAuthorization { public async getFindAuthorizationFilter( authorizationEntity: AlertingAuthorizationEntity, - filterOpts: AlertingAuthorizationFilterOpts + filterOpts: AlertingAuthorizationFilterOpts, + featuresIds?: Set ): Promise<{ filter?: KueryNode | JsonObject; ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; }> { - return this.getAuthorizationFilter(authorizationEntity, filterOpts, ReadOperations.Find); + return this.getAuthorizationFilter( + authorizationEntity, + filterOpts, + ReadOperations.Find, + featuresIds + ); } public async getAuthorizedRuleTypes( @@ -232,7 +240,8 @@ export class AlertingAuthorization { public async getAuthorizationFilter( authorizationEntity: AlertingAuthorizationEntity, filterOpts: AlertingAuthorizationFilterOpts, - operation: WriteOperations | ReadOperations + operation: WriteOperations | ReadOperations, + featuresIds?: Set ): Promise<{ filter?: KueryNode | JsonObject; ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; @@ -241,7 +250,8 @@ export class AlertingAuthorization { const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( this.ruleTypeRegistry.list(), [operation], - authorizationEntity + authorizationEntity, + featuresIds ); if (!authorizedRuleTypes.size) { @@ -326,6 +336,9 @@ export class AlertingAuthorization { string, [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] >(); + const allPossibleConsumers = await this.allPossibleConsumers; + const addLegacyConsumerPrivileges = (legacyConsumer: string) => + legacyConsumer === ALERTS_FEATURE_ID || isEmpty(featuresIds); for (const feature of fIds) { const featureDef = this.features .getKibanaFeatures() @@ -351,6 +364,31 @@ export class AlertingAuthorization { ruleTypeAuth.producer === feature, ] ); + // FUTURE ENGINEER + // We are just trying to add back the legacy consumers associated + // to the rule type to get back the privileges that was given at one point + if (!isEmpty(ruleTypeAuth.validLegacyConsumers)) { + ruleTypeAuth.validLegacyConsumers.forEach((legacyConsumer) => { + if (addLegacyConsumerPrivileges(legacyConsumer)) { + if (!allPossibleConsumers[legacyConsumer]) { + allPossibleConsumers[legacyConsumer] = { + read: true, + all: true, + }; + } + + privilegeToRuleType.set( + this.authorization!.actions.alerting.get( + ruleTypeId, + legacyConsumer, + authorizationEntity, + operation + ), + [ruleTypeAuth, legacyConsumer, hasPrivilegeByOperation(operation), false] + ); + } + }); + } } } } @@ -368,7 +406,7 @@ export class AlertingAuthorization { ? // has access to all features this.augmentWithAuthorizedConsumers( new Set(ruleTypesAuthorized.values()), - await this.allPossibleConsumers + allPossibleConsumers ) : // only has some of the required privileges privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => { @@ -383,10 +421,14 @@ export class AlertingAuthorization { if (isAuthorizedAtProducerLevel) { // granting privileges under the producer automatically authorized the Rules Management UI as well - ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[ALERTS_FEATURE_ID] - ); + ruleType.validLegacyConsumers.forEach((legacyConsumer) => { + if (addLegacyConsumerPrivileges(legacyConsumer)) { + ruleType.authorizedConsumers[legacyConsumer] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[legacyConsumer] + ); + } + }); } authorizedRuleTypes.add(ruleType); } @@ -438,7 +480,10 @@ function asAuthorizedConsumers( consumers: string[], hasPrivileges: HasPrivileges ): AuthorizedConsumers { - return fromPairs(consumers.map((feature) => [feature, hasPrivileges])); + return consumers.reduce((acc, feature) => { + acc[feature] = hasPrivileges; + return acc; + }, {}); } function getUnauthorizedMessage( @@ -449,3 +494,16 @@ function getUnauthorizedMessage( ): string { return `Unauthorized by "${scope}" to ${operation} "${alertTypeId}" ${entity}`; } + +export const getValidConsumer = ({ + validLegacyConsumers, + legacyConsumer, + producer, +}: { + validLegacyConsumers: string[]; + legacyConsumer: string; + producer: string; +}): string => + legacyConsumer === ALERTS_FEATURE_ID || validLegacyConsumers.includes(legacyConsumer) + ? producer + : legacyConsumer; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index c9e129085c3c0..bfa36ccfd46c5 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -34,6 +34,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -71,6 +72,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -111,6 +113,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -130,6 +133,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -149,6 +153,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -189,6 +194,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -208,6 +214,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -249,6 +256,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -268,6 +276,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -303,6 +312,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -339,6 +349,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -403,6 +414,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -475,6 +487,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -494,6 +507,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { actionGroups: [], @@ -513,6 +527,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { @@ -678,6 +693,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]), { diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 007cd4481bd7e..02fa23fbb8cdb 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -46,6 +46,7 @@ const ruleType: jest.Mocked = { validate: { params: schema.any(), }, + validLegacyConsumers: [], }; const context: RuleContextOpts = { diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index 6b18d1aac93dd..5aec760a1eadf 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -26,6 +26,7 @@ describe('createAlertEventLogRecordObject', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], }; test('created alert event "execute-start"', async () => { diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts index c9bfbdca4aa0b..24e73f9620582 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts @@ -237,6 +237,7 @@ describe('aggregateRulesRoute', () => { }, "options": Object { "defaultSearchOperator": "AND", + "filterConsumers": undefined, }, }, ] diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index ea3bb22bd0d17..2e10bb1ba31a1 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -38,16 +38,19 @@ const querySchema = schema.object({ ) ), filter: schema.maybe(schema.string()), + filter_consumers: schema.maybe(schema.arrayOf(schema.string())), }); const rewriteQueryReq: RewriteRequestCase = ({ default_search_operator: defaultSearchOperator, has_reference: hasReference, search_fields: searchFields, + filter_consumers: filterConsumers, ...rest }) => ({ ...rest, defaultSearchOperator, + filterConsumers, ...(hasReference ? { hasReference } : {}), ...(searchFields ? { searchFields } : {}), }); diff --git a/x-pack/plugins/alerting/server/routes/find_rules.test.ts b/x-pack/plugins/alerting/server/routes/find_rules.test.ts index 6e8f4e5474dbf..afd621c6e7abd 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.test.ts @@ -79,6 +79,7 @@ describe('findRulesRoute', () => { "includeSnoozeData": true, "options": Object { "defaultSearchOperator": "OR", + "filterConsumers": undefined, "page": 1, "perPage": 1, }, diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index 04b18da1a1b0c..1eab92db82383 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -47,6 +47,7 @@ const querySchema = schema.object({ ), fields: schema.maybe(schema.arrayOf(schema.string())), filter: schema.maybe(schema.string()), + filter_consumers: schema.maybe(schema.arrayOf(schema.string())), }); const rewriteQueryReq: RewriteRequestCase = ({ @@ -56,11 +57,13 @@ const rewriteQueryReq: RewriteRequestCase = ({ per_page: perPage, sort_field: sortField, sort_order: sortOrder, + filter_consumers: filterConsumers, ...rest }) => ({ ...rest, defaultSearchOperator, perPage, + filterConsumers, ...(sortField ? { sortField } : {}), ...(sortOrder ? { sortOrder } : {}), ...(hasReference ? { hasReference } : {}), diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index b1c562ada8717..bb42109ef8502 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -51,6 +51,7 @@ const ruleTypes = [ defaultScheduleInterval: '10m', hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.test.ts b/x-pack/plugins/alerting/server/routes/legacy/health.test.ts index 9946f659a0744..e25485229dc0c 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/health.test.ts @@ -55,6 +55,7 @@ const ruleTypes = [ defaultScheduleInterval: '10m', hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; diff --git a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts index 7262f42319c1f..ae094df73d093 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts @@ -64,6 +64,7 @@ describe('listAlertTypesRoute', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; rulesClient.listRuleTypes.mockResolvedValueOnce(new Set(listTypes)); @@ -98,6 +99,7 @@ describe('listAlertTypesRoute', () => { "id": "recovered", "name": "Recovered", }, + "validLegacyConsumers": Array [], }, ], } @@ -143,6 +145,7 @@ describe('listAlertTypesRoute', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; @@ -198,6 +201,7 @@ describe('listAlertTypesRoute', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index 2dab9284bb5ac..c52ce30e41dcd 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -62,9 +62,12 @@ describe('ruleTypesRoute', () => { doesSetRecoveryContext: false, hasAlertsMappings: true, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; - const expectedResult: Array> = [ + const expectedResult: Array< + AsApiContract> + > = [ { id: '1', name: 'name', @@ -172,6 +175,7 @@ describe('ruleTypesRoute', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; @@ -227,6 +231,7 @@ describe('ruleTypesRoute', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], } as RegistryAlertTypeWithAuth, ]; diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index 4c4a812d75684..4521bdfd167a2 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -8,10 +8,10 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState } from '../lib'; import { RegistryAlertTypeWithAuth } from '../authorization'; -import { RewriteResponseCase, verifyAccessAndContext } from './lib'; +import { verifyAccessAndContext } from './lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; -const rewriteBodyRes: RewriteResponseCase = (results) => { +const rewriteBodyRes = (results: RegistryAlertTypeWithAuth[]) => { return results.map( ({ enabledInLicense, @@ -27,8 +27,9 @@ const rewriteBodyRes: RewriteResponseCase = (result doesSetRecoveryContext, hasAlertsMappings, hasFieldsForAAD, + validLegacyConsumers, ...rest - }) => ({ + }: RegistryAlertTypeWithAuth) => ({ ...rest, enabled_in_license: enabledInLicense, recovery_action_group: recoveryActionGroup, diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 689741c7479ac..05aa84292ed8d 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -590,40 +590,41 @@ describe('Create Lifecycle', () => { }); const ruleType = registry.get('test'); expect(ruleType).toMatchInlineSnapshot(` - Object { - "actionGroups": Array [ - Object { - "id": "default", - "name": "Default", - }, - Object { - "id": "recovered", - "name": "Recovered", - }, - ], - "actionVariables": Object { - "context": Array [], - "params": Array [], - "state": Array [], - }, - "defaultActionGroupId": "default", - "executor": [MockFunction], - "id": "test", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "Test", - "producer": "alerts", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validate": Object { - "params": Object { - "validate": [Function], - }, - }, - } - `); + Object { + "actionGroups": Array [ + Object { + "id": "default", + "name": "Default", + }, + Object { + "id": "recovered", + "name": "Recovered", + }, + ], + "actionVariables": Object { + "context": Array [], + "params": Array [], + "state": Array [], + }, + "defaultActionGroupId": "default", + "executor": [MockFunction], + "id": "test", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "Test", + "producer": "alerts", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + "validate": Object { + "params": Object { + "validate": [Function], + }, + }, + } + `); }); test(`should throw an error if type isn't registered`, () => { @@ -713,6 +714,7 @@ describe('Create Lifecycle', () => { "name": "Recovered", }, "ruleTaskTimeout": "20m", + "validLegacyConsumers": Array [], }, } `); diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index c0339275bcf35..68e3e1d3d590b 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -38,6 +38,7 @@ import { getRuleTypeFeatureUsageName } from './lib/get_rule_type_feature_usage_n import { InMemoryMetrics } from './monitoring'; import { AlertingRulesConfig } from '.'; import { AlertsService } from './alerts_service/alerts_service'; +import { getRuleTypeIdValidLegacyConsumers } from './rule_type_registry_deprecated_consumers'; export interface ConstructorOptions { logger: Logger; @@ -70,6 +71,7 @@ export interface RegistryRuleType enabledInLicense: boolean; hasFieldsForAAD: boolean; hasAlertsMappings: boolean; + validLegacyConsumers: string[]; } /** @@ -102,6 +104,7 @@ export type NormalizedRuleType< RecoveryActionGroupId extends string, AlertData extends RuleAlertData > = { + validLegacyConsumers: string[]; actionGroups: Array>; } & Omit< RuleType< @@ -386,6 +389,7 @@ export class RuleTypeRegistry { doesSetRecoveryContext, alerts, fieldsForAAD, + validLegacyConsumers, }, ]) => { // KEEP the type here to be safe if not the map is ignoring it for some reason @@ -409,6 +413,7 @@ export class RuleTypeRegistry { ).isValid, hasFieldsForAAD: Boolean(fieldsForAAD), hasAlertsMappings: !!alerts, + validLegacyConsumers, ...(alerts ? { alerts } : {}), }; return ruleType; @@ -499,5 +504,6 @@ function augmentActionGroupsWithReserved< ...ruleType, actionGroups: [...actionGroups, ...reservedActionGroups], recoveryActionGroup: recoveryActionGroup ?? RecoveredActionGroup, + validLegacyConsumers: getRuleTypeIdValidLegacyConsumers(id), }; } diff --git a/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.test.ts b/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.test.ts new file mode 100644 index 0000000000000..e1a7828d85042 --- /dev/null +++ b/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { + getRuleTypeIdValidLegacyConsumers, + ruleTypeIdWithValidLegacyConsumers, +} from './rule_type_registry_deprecated_consumers'; + +describe('rule_type_registry_deprecated_consumers', () => { + describe('ruleTypeIdWithValidLegacyConsumers', () => { + test('Only these rule type ids should be in the list', () => { + expect(Object.keys(ruleTypeIdWithValidLegacyConsumers)).toMatchInlineSnapshot(` + Array [ + "example.always-firing", + "transform_health", + ".index-threshold", + ".geo-containment", + ".es-query", + "xpack.ml.anomaly_detection_alert", + "xpack.ml.anomaly_detection_jobs_health", + "xpack.synthetics.alerts.monitorStatus", + "xpack.synthetics.alerts.tls", + "xpack.uptime.alerts.monitorStatus", + "xpack.uptime.alerts.tlsCertificate", + "xpack.uptime.alerts.durationAnomaly", + "xpack.uptime.alerts.tls", + "siem.eqlRule", + "siem.savedQueryRule", + "siem.indicatorRule", + "siem.mlRule", + "siem.queryRule", + "siem.thresholdRule", + "siem.newTermsRule", + "siem.notifications", + "slo.rules.burnRate", + "metrics.alert.anomaly", + "logs.alert.document.count", + "metrics.alert.inventory.threshold", + "metrics.alert.threshold", + "monitoring_alert_cluster_health", + "monitoring_alert_license_expiration", + "monitoring_alert_cpu_usage", + "monitoring_alert_missing_monitoring_data", + "monitoring_alert_disk_usage", + "monitoring_alert_thread_pool_search_rejections", + "monitoring_alert_thread_pool_write_rejections", + "monitoring_alert_jvm_memory_usage", + "monitoring_alert_nodes_changed", + "monitoring_alert_logstash_version_mismatch", + "monitoring_alert_kibana_version_mismatch", + "monitoring_alert_elasticsearch_version_mismatch", + "monitoring_ccr_read_exceptions", + "monitoring_shard_size", + "apm.transaction_duration", + "apm.anomaly", + "apm.error_rate", + "apm.transaction_error_rate", + "test.always-firing", + "test.always-firing-alert-as-data", + "test.authorization", + "test.cancellableRule", + "test.cumulative-firing", + "test.exceedsAlertLimit", + "test.failing", + "test.gold.noop", + "test.longRunning", + "test.multipleSearches", + "test.never-firing", + "test.noop", + "test.onlyContextVariables", + "test.onlyStateVariables", + "test.patternFiring", + "test.patternFiringAad", + "test.patternFiringAutoRecoverFalse", + "test.patternLongRunning", + "test.patternLongRunning.cancelAlertsOnRuleTimeout", + "test.patternSuccessOrFailure", + "test.restricted-noop", + "test.throw", + "test.unrestricted-noop", + "test.validation", + ] + `); + }); + }); + describe('getRuleTypeIdValidLegacyConsumers', () => { + test('".es-query" should have "alerts" & "discover" as legacy consumers', () => { + expect(getRuleTypeIdValidLegacyConsumers('.es-query')).toEqual(['alerts', 'discover']); + }); + + test('All other rule types except ".es-query" should have "alerts" as legacy consumer', () => { + for (const ruleTypeId of Object.keys(ruleTypeIdWithValidLegacyConsumers).filter( + (rt) => rt !== '.es-query' + )) { + expect(getRuleTypeIdValidLegacyConsumers(ruleTypeId)).toEqual(['alerts']); + } + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.ts b/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.ts new file mode 100644 index 0000000000000..d6a238c414243 --- /dev/null +++ b/x-pack/plugins/alerting/server/rule_type_registry_deprecated_consumers.ts @@ -0,0 +1,88 @@ +/* + * 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 { ALERTS_FEATURE_ID } from './types'; + +export const ruleTypeIdWithValidLegacyConsumers: Record = { + 'example.always-firing': [ALERTS_FEATURE_ID], + transform_health: [ALERTS_FEATURE_ID], + '.index-threshold': [ALERTS_FEATURE_ID], + '.geo-containment': [ALERTS_FEATURE_ID], + '.es-query': [ALERTS_FEATURE_ID, 'discover'], + 'xpack.ml.anomaly_detection_alert': [ALERTS_FEATURE_ID], + 'xpack.ml.anomaly_detection_jobs_health': [ALERTS_FEATURE_ID], + 'xpack.synthetics.alerts.monitorStatus': [ALERTS_FEATURE_ID], + 'xpack.synthetics.alerts.tls': [ALERTS_FEATURE_ID], + 'xpack.uptime.alerts.monitorStatus': [ALERTS_FEATURE_ID], + 'xpack.uptime.alerts.tlsCertificate': [ALERTS_FEATURE_ID], + 'xpack.uptime.alerts.durationAnomaly': [ALERTS_FEATURE_ID], + 'xpack.uptime.alerts.tls': [ALERTS_FEATURE_ID], + 'siem.eqlRule': [ALERTS_FEATURE_ID], + 'siem.savedQueryRule': [ALERTS_FEATURE_ID], + 'siem.indicatorRule': [ALERTS_FEATURE_ID], + 'siem.mlRule': [ALERTS_FEATURE_ID], + 'siem.queryRule': [ALERTS_FEATURE_ID], + 'siem.thresholdRule': [ALERTS_FEATURE_ID], + 'siem.newTermsRule': [ALERTS_FEATURE_ID], + 'siem.notifications': [ALERTS_FEATURE_ID], + 'slo.rules.burnRate': [ALERTS_FEATURE_ID], + 'metrics.alert.anomaly': [ALERTS_FEATURE_ID], + 'logs.alert.document.count': [ALERTS_FEATURE_ID], + 'metrics.alert.inventory.threshold': [ALERTS_FEATURE_ID], + 'metrics.alert.threshold': [ALERTS_FEATURE_ID], + monitoring_alert_cluster_health: [ALERTS_FEATURE_ID], + monitoring_alert_license_expiration: [ALERTS_FEATURE_ID], + monitoring_alert_cpu_usage: [ALERTS_FEATURE_ID], + monitoring_alert_missing_monitoring_data: [ALERTS_FEATURE_ID], + monitoring_alert_disk_usage: [ALERTS_FEATURE_ID], + monitoring_alert_thread_pool_search_rejections: [ALERTS_FEATURE_ID], + monitoring_alert_thread_pool_write_rejections: [ALERTS_FEATURE_ID], + monitoring_alert_jvm_memory_usage: [ALERTS_FEATURE_ID], + monitoring_alert_nodes_changed: [ALERTS_FEATURE_ID], + monitoring_alert_logstash_version_mismatch: [ALERTS_FEATURE_ID], + monitoring_alert_kibana_version_mismatch: [ALERTS_FEATURE_ID], + monitoring_alert_elasticsearch_version_mismatch: [ALERTS_FEATURE_ID], + monitoring_ccr_read_exceptions: [ALERTS_FEATURE_ID], + monitoring_shard_size: [ALERTS_FEATURE_ID], + 'apm.transaction_duration': [ALERTS_FEATURE_ID], + 'apm.anomaly': [ALERTS_FEATURE_ID], + 'apm.error_rate': [ALERTS_FEATURE_ID], + 'apm.transaction_error_rate': [ALERTS_FEATURE_ID], + 'test.always-firing': [ALERTS_FEATURE_ID], + 'test.always-firing-alert-as-data': [ALERTS_FEATURE_ID], + 'test.authorization': [ALERTS_FEATURE_ID], + 'test.cancellableRule': [ALERTS_FEATURE_ID], + 'test.cumulative-firing': [ALERTS_FEATURE_ID], + 'test.exceedsAlertLimit': [ALERTS_FEATURE_ID], + 'test.failing': [ALERTS_FEATURE_ID], + 'test.gold.noop': [ALERTS_FEATURE_ID], + 'test.longRunning': [ALERTS_FEATURE_ID], + 'test.multipleSearches': [ALERTS_FEATURE_ID], + 'test.never-firing': [ALERTS_FEATURE_ID], + 'test.noop': [ALERTS_FEATURE_ID], + 'test.onlyContextVariables': [ALERTS_FEATURE_ID], + 'test.onlyStateVariables': [ALERTS_FEATURE_ID], + 'test.patternFiring': [ALERTS_FEATURE_ID], + 'test.patternFiringAad': [ALERTS_FEATURE_ID], + 'test.patternFiringAutoRecoverFalse': [ALERTS_FEATURE_ID], + 'test.patternLongRunning': [ALERTS_FEATURE_ID], + 'test.patternLongRunning.cancelAlertsOnRuleTimeout': [ALERTS_FEATURE_ID], + 'test.patternSuccessOrFailure': [ALERTS_FEATURE_ID], + 'test.restricted-noop': [ALERTS_FEATURE_ID], + 'test.throw': [ALERTS_FEATURE_ID], + 'test.unrestricted-noop': [ALERTS_FEATURE_ID], + 'test.validation': [ALERTS_FEATURE_ID], +}; + +const getRuleTypeIdValidLegacyConsumers = (ruleTypeId: string): string[] => { + if (ruleTypeIdWithValidLegacyConsumers[ruleTypeId]) { + return ruleTypeIdWithValidLegacyConsumers[ruleTypeId]; + } + return []; +}; + +export { getRuleTypeIdValidLegacyConsumers }; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts index e0bbdb0d770ec..5e6e958f3bbc0 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts @@ -46,6 +46,7 @@ const ruleType: jest.Mocked = { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }; const context = { diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts index 84f0a89e72c01..d01d45abc9759 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts @@ -32,6 +32,7 @@ describe('validateActions', () => { context: 'context', mappings: { fieldMap: { field: { type: 'fieldType', required: false } } }, }, + validLegacyConsumers: [], }; const data = { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts index 6dd26c1a7e197..69ad4edaa42f3 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts @@ -7,6 +7,7 @@ import { KueryNode, nodeBuilder } from '@kbn/es-query'; import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { isEmpty } from 'lodash'; import { AlertingAuthorizationEntity } from '../../authorization'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { buildKueryNodeFilter } from '../common'; @@ -25,13 +26,14 @@ export async function aggregate>( params: AggregateParams ): Promise { const { options = {}, aggs } = params; - const { filter, page = 1, perPage = 0, ...restOptions } = options; + const { filter, page = 1, perPage = 0, filterConsumers, ...restOptions } = options; let authorizationTuple; try { authorizationTuple = await context.authorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts + alertingAuthorizationFilterOpts, + isEmpty(filterConsumers) ? undefined : new Set(filterConsumers) ); validateRuleAggregationFields(aggs); } catch (error) { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/find.ts b/x-pack/plugins/alerting/server/rules_client/methods/find.ts index 6493174a61b6a..537dfef55aff0 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/find.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/find.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { pick } from 'lodash'; +import { isEmpty, pick } from 'lodash'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { RawRule, RuleTypeParams, SanitizedRule, Rule } from '../../types'; @@ -34,6 +34,7 @@ export interface FindParams { options?: FindOptions; excludeFromPublicApi?: boolean; includeSnoozeData?: boolean; + featuresIds?: string[]; } export interface FindOptions extends IndexType { @@ -50,6 +51,7 @@ export interface FindOptions extends IndexType { }; fields?: string[]; filter?: string | KueryNode; + filterConsumers?: string[]; } export interface FindResult { @@ -62,7 +64,7 @@ export interface FindResult { export async function find( context: RulesClientContext, { - options: { fields, ...options } = {}, + options: { fields, filterConsumers, ...options } = {}, excludeFromPublicApi = false, includeSnoozeData = false, }: FindParams = {} @@ -71,7 +73,8 @@ export async function find( try { authorizationTuple = await context.authorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts + alertingAuthorizationFilterOpts, + isEmpty(filterConsumers) ? undefined : new Set(filterConsumers) ); } catch (error) { context.auditLogger?.log( @@ -84,7 +87,6 @@ export async function find( } const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple; - const filterKueryNode = buildKueryNodeFilter(options.filter); let sortField = mapSortField(options.sortField); if (excludeFromPublicApi) { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index c45e74da45999..ab5b1d7bcc16d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -76,6 +76,7 @@ describe('aggregate()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]); beforeEach(() => { @@ -160,6 +161,7 @@ describe('aggregate()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]) ); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts index dbe04ec420000..70911dcde76d2 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts @@ -178,6 +178,7 @@ describe('bulkDelete', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 4b1a6fc2eba8c..03e4cfb50d514 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -84,6 +84,7 @@ describe('find()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]); beforeEach(() => { @@ -146,6 +147,7 @@ describe('find()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]) ); @@ -459,6 +461,7 @@ describe('find()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]) ); @@ -477,6 +480,7 @@ describe('find()', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], })); ruleTypeRegistry.get.mockImplementationOnce(() => ({ id: '123', @@ -497,6 +501,7 @@ describe('find()', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], })); unsecuredSavedObjectsClient.find.mockResolvedValue({ total: 2, @@ -667,6 +672,7 @@ describe('find()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]) ); @@ -685,6 +691,7 @@ describe('find()', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], })); ruleTypeRegistry.get.mockImplementationOnce(() => ({ id: '123', @@ -705,6 +712,7 @@ describe('find()', () => { validate: { params: schema.any(), }, + validLegacyConsumers: [], })); unsecuredSavedObjectsClient.find.mockResolvedValue({ total: 2, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts index 20af52838652a..a2210f56af291 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts @@ -312,6 +312,7 @@ describe('get()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const rulesClient = new RulesClient(rulesClientParams); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ @@ -438,6 +439,7 @@ describe('get()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const rulesClient = new RulesClient(rulesClientParams); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts index 1f0c4f405f2c2..31eb7bd5bafad 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts @@ -63,6 +63,7 @@ const listedTypes = new Set([ enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]); @@ -116,6 +117,7 @@ describe('getTags()', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]) ); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts index 25afdedf54d3c..ee84a789454ef 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts @@ -122,6 +122,7 @@ export function getBeforeSetup( validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); rulesClientParams.getEventLogClient.mockResolvedValue( eventLogClient ?? eventLogClientMock.create() diff --git a/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts index 9c8c78f2753f4..c8b69f632fd08 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts @@ -69,6 +69,7 @@ describe('listRuleTypes', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const myAppAlertType: RegistryRuleType = { actionGroups: [], @@ -83,6 +84,7 @@ describe('listRuleTypes', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }; const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); @@ -127,6 +129,7 @@ describe('listRuleTypes', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, { id: 'myOtherType', @@ -140,6 +143,7 @@ describe('listRuleTypes', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]); beforeEach(() => { @@ -163,6 +167,7 @@ describe('listRuleTypes', () => { enabledInLicense: true, hasAlertsMappings: false, hasFieldsForAAD: false, + validLegacyConsumers: [], }, ]); authorization.filterByRuleTypeAuthorization.mockResolvedValue(authorizedTypes); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts index 37372752d53a3..1ebdabaf4cf6a 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -287,6 +287,7 @@ describe('resolve()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const rulesClient = new RulesClient(rulesClientParams); unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ @@ -423,6 +424,7 @@ describe('resolve()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); const rulesClient = new RulesClient(rulesClientParams); unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 26350664b8445..7dadc787f7710 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -175,6 +175,7 @@ describe('update()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }); (migrateLegacyActions as jest.Mock).mockResolvedValue({ hasLegacyActions: false, @@ -997,6 +998,7 @@ describe('update()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', @@ -1512,6 +1514,7 @@ describe('update()', () => { return { state: {} }; }, producer: 'alerts', + validLegacyConsumers: [], }); await expect( rulesClient.update({ @@ -1896,6 +1899,7 @@ describe('update()', () => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: alertId, diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index fcde2f6e4f444..a996598655f91 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -362,6 +362,7 @@ beforeEach(() => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], })); ruleTypeRegistry.get.mockReturnValue({ @@ -379,6 +380,7 @@ beforeEach(() => { validate: { params: { validate: (params) => params }, }, + validLegacyConsumers: [], }); rulesClient = new RulesClient(rulesClientParams); diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index ce86dd4756093..de77ebd536549 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -80,6 +80,7 @@ const ruleType: NormalizedRuleType< mappings: { fieldMap: { field: { type: 'fieldType', required: false } } }, }, autoRecoverAlerts: false, + validLegacyConsumers: [], }; const rule = { id: '1', diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 467d7460afc2b..40a8e65a1f009 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -152,6 +152,7 @@ export const ruleType: jest.Mocked = { context: 'test', mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, }, + validLegacyConsumers: [], }; export const mockRunNowResponse = { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 3cfa7dde78341..786a0fe4a338f 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -63,6 +63,7 @@ const ruleType: UntypedNormalizedRuleType = { validate: { params: schema.any(), }, + validLegacyConsumers: [], }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 8311fc43fcb57..fe22261fd57f7 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -12,8 +12,11 @@ import { LicensingPluginSetup, LicensingApiRequestHandlerContext, } from '@kbn/licensing-plugin/server'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; -import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; + +import { + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices'; import { ApmRuleType, diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index adbcf96345dfe..1b06fa44a8dd5 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -96,7 +96,6 @@ "@kbn/unified-field-list", "@kbn/discover-plugin", "@kbn/observability-ai-assistant-plugin", - "@kbn/stack-alerts-plugin", "@kbn/apm-data-access-plugin" ], "exclude": ["target/**/*"] diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 5ab73d95c15de..3edffdda86868 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -680,15 +680,11 @@ Array [ "privilege": Object { "alerting": Object { "alert": Object { - "all": Array [ - ".es-query", - ], + "all": Array [], "read": Array [], }, "rule": Object { - "all": Array [ - ".es-query", - ], + "all": Array [], "read": Array [], }, }, @@ -739,18 +735,6 @@ Array [ }, Object { "privilege": Object { - "alerting": Object { - "alert": Object { - "all": Array [ - ".es-query", - ], - }, - "rule": Object { - "all": Array [ - ".es-query", - ], - }, - }, "app": Array [ "discover", "kibana", @@ -1284,15 +1268,11 @@ Array [ "privilege": Object { "alerting": Object { "alert": Object { - "all": Array [ - ".es-query", - ], + "all": Array [], "read": Array [], }, "rule": Object { - "all": Array [ - ".es-query", - ], + "all": Array [], "read": Array [], }, }, @@ -1343,18 +1323,6 @@ Array [ }, Object { "privilege": Object { - "alerting": Object { - "alert": Object { - "all": Array [ - ".es-query", - ], - }, - "rule": Object { - "all": Array [ - ".es-query", - ], - }, - }, "app": Array [ "discover", "kibana", diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 076ef4941c530..4097780cdfe10 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -32,7 +32,6 @@ export const buildOSSFeatures = ({ category: DEFAULT_APP_CATEGORIES.kibana, app: ['discover', 'kibana'], catalogue: ['discover'], - alerting: ['.es-query'], privileges: { all: { app: ['discover', 'kibana'], @@ -43,14 +42,6 @@ export const buildOSSFeatures = ({ read: ['index-pattern'], }, ui: ['show', 'save', 'saveQuery'], - alerting: { - rule: { - all: ['.es-query'], - }, - alert: { - all: ['.es-query'], - }, - }, }, read: { app: ['discover', 'kibana'], @@ -60,14 +51,6 @@ export const buildOSSFeatures = ({ read: ['index-pattern', 'search', 'query'], }, ui: ['show'], - alerting: { - rule: { - all: ['.es-query'], - }, - alert: { - all: ['.es-query'], - }, - }, }, }, subFeatures: [ diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 3c95fb71324c0..49d1c1dff7544 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { logViewSavedObjectName } from '@kbn/logs-shared-plugin/server'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; -import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { ES_QUERY_ID } from '@kbn/rule-data-utils'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 31643d4a1235b..0ace444a00e99 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -70,7 +70,6 @@ "@kbn/licensing-plugin", "@kbn/aiops-utils", "@kbn/lens-embeddable-utils", - "@kbn/stack-alerts-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability/common/constants.ts b/x-pack/plugins/observability/common/constants.ts index 20e121595489e..ef3bdea149ba7 100644 --- a/x-pack/plugins/observability/common/constants.ts +++ b/x-pack/plugins/observability/common/constants.ts @@ -9,10 +9,8 @@ import { i18n } from '@kbn/i18n'; import { AlertConsumers } from '@kbn/rule-data-utils'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { RuleCreationValidConsumer } from '@kbn/triggers-actions-ui-plugin/public'; -import { STACK_ALERTS_FEATURE_ID } from '@kbn/stack-alerts-plugin/common'; export const SLO_BURN_RATE_RULE_TYPE_ID = 'slo.rules.burnRate'; -export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; export const ALERT_STATUS_ALL = 'all'; @@ -65,5 +63,4 @@ export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[ AlertConsumers.LOGS, AlertConsumers.UPTIME, AlertConsumers.SLO, - STACK_ALERTS_FEATURE_ID, ]; diff --git a/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx b/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx index e0d24d58eb0db..e270ce8b8b143 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/alert_flyout.tsx @@ -6,12 +6,14 @@ */ import React, { useCallback, useContext, useMemo } from 'react'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '../../../../common/constants'; + +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { MetricsExplorerSeries } from '../../../../common/threshold_rule/metrics_explorer'; import { TriggerActionsContext } from './triggers_actions_context'; import { useAlertPrefillContext } from '../helpers/use_alert_prefill'; import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; +import { observabilityRuleCreationValidConsumers } from '../../../../common/constants'; interface Props { visible?: boolean; @@ -28,7 +30,7 @@ export function AlertFlyout(props: Props) { () => triggersActionsUI && triggersActionsUI.getAddRuleFlyout({ - consumer: 'alerts', + consumer: 'logs', onClose: onCloseFlyout, canChangeTrigger: false, ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, @@ -36,6 +38,7 @@ export function AlertFlyout(props: Props) { currentOptions: props.options, series: props.series, }, + validConsumers: observabilityRuleCreationValidConsumers, }), // eslint-disable-next-line react-hooks/exhaustive-deps [triggersActionsUI, onCloseFlyout] diff --git a/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts b/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts index 9ed239f507ea2..834cc31f39d2e 100644 --- a/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts +++ b/x-pack/plugins/observability/public/hooks/use_get_filtered_rule_types.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; +import { ES_QUERY_ID } from '@kbn/rule-data-utils'; import { usePluginContext } from './use_plugin_context'; export function useGetFilteredRuleTypes() { diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index c09cc504e62db..a95cdce7f9948 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -31,10 +31,12 @@ import { } from '../../components/alert_search_bar/containers'; import { calculateTimeRangeBucketSize } from '../overview/helpers/calculate_bucket_size'; import { getAlertSummaryTimeRange } from '../../utils/alert_summary_widget'; -import { observabilityAlertFeatureIds } from '../../../common/constants'; +import { + observabilityAlertFeatureIds, + observabilityRuleCreationValidConsumers, +} from '../../../common/constants'; import { ALERTS_URL_STORAGE_KEY } from '../../../common/constants'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; -import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y'; const ALERTS_PER_PAGE = 50; @@ -131,6 +133,7 @@ function InternalAlertsPage() { const response = await loadRuleAggregations({ http, typesFilter: filteredRuleTypes, + filterConsumers: observabilityRuleCreationValidConsumers, }); const { ruleExecutionStatus, ruleMutedStatus, ruleEnabledStatus, ruleSnoozedStatus } = response; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx index 14afd4994b2be..3b883febf3add 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx @@ -20,8 +20,7 @@ import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import { AttachmentType } from '@kbn/cases-plugin/common'; import { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; - -import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { ALERT_RULE_TYPE_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { useKibana } from '../../../utils/kibana_react'; import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions'; @@ -32,7 +31,6 @@ import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; import type { ObservabilityRuleTypeRegistry } from '../../..'; import type { ConfigSchema } from '../../../plugin'; import type { TopAlert } from '../../../typings/alerts'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '../../../../common/constants'; const ALERT_DETAILS_PAGE_ID = 'alert-details-o11y'; diff --git a/x-pack/plugins/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability/public/pages/rules/rules.tsx index 2c2ef3463b0ea..700f348842757 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.tsx @@ -150,6 +150,7 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { setRefresh(new Date()); return Promise.resolve(); }} + useRuleProducer={true} /> )} diff --git a/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx index 317091e81fb29..22b321262943c 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx @@ -11,6 +11,7 @@ import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { RuleStatus } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../utils/kibana_react'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; +import { observabilityRuleCreationValidConsumers } from '../../../common/constants'; interface RulesTabProps { setRefresh: React.Dispatch>; @@ -73,6 +74,7 @@ export function RulesTab({ setRefresh, stateRefresh }: RulesTabProps) { return ( { @@ -137,6 +137,7 @@ interface SingleSearchAfterAndAudit { operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; sort?: estypes.SortOptions[] | undefined; lastSortIds?: Array | undefined; + featureIds?: string[]; } /** @@ -152,6 +153,7 @@ export class AlertsClient { private readonly spaceId: string | undefined; private readonly ruleDataService: IRuleDataService; private readonly getRuleType: RuleTypeRegistry['get']; + private getAlertIndicesAlias!: AlertingStart['getAlertIndicesAlias']; constructor(options: ConstructorOptions) { this.logger = options.logger; @@ -163,6 +165,7 @@ export class AlertsClient { this.spaceId = this.authorization.getSpaceId(); this.ruleDataService = options.ruleDataService; this.getRuleType = options.getRuleType; + this.getAlertIndicesAlias = options.getAlertIndicesAlias; } private getOutcome( @@ -281,6 +284,7 @@ export class AlertsClient { operation, sort, lastSortIds = [], + featureIds, }: SingleSearchAfterAndAudit) { try { const alertSpaceId = this.spaceId; @@ -294,7 +298,14 @@ export class AlertsClient { let queryBody: estypes.SearchRequest['body'] = { fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS], - query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config), + query: await this.buildEsQueryWithAuthz( + query, + id, + alertSpaceId, + operation, + config, + featureIds ? new Set(featureIds) : undefined + ), aggs, _source, track_total_hits: trackTotalHits, @@ -433,10 +444,15 @@ export class AlertsClient { id: string | null | undefined, alertSpaceId: string, operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find, - config: EsQueryConfig + config: EsQueryConfig, + featuresIds?: Set ) { try { - const authzFilter = (await getAuthzFilter(this.authorization, operation)) as Filter; + const authzFilter = (await getAuthzFilter( + this.authorization, + operation, + featuresIds + )) as Filter; const spacesFilter = getSpacesFilter(alertSpaceId) as unknown as Filter; let esQuery; if (id != null) { @@ -681,6 +697,7 @@ export class AlertsClient { }, }, size: 0, + featureIds, }); let activeAlertCount = 0; @@ -1006,35 +1023,16 @@ export class AlertsClient { public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { try { - // ATTENTION FUTURE DEVELOPER when you are a super user the augmentedRuleTypes.authorizedRuleTypes will - // return all of the features that you can access and does not care about your featureIds - const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization( - featureIds, - [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], - AlertingAuthorizationEntity.Alert + const authorizedRuleTypes = await this.authorization.getAuthorizedRuleTypes( + AlertingAuthorizationEntity.Alert, + new Set(featureIds) ); - // 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 = new Set(); - for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) { - authorizedFeatures.add(ruleType.producer); - } - const validAuthorizedFeatures = Array.from(authorizedFeatures).filter( - (feature): feature is ValidFeatureId => - featureIds.includes(feature) && isValidFeatureId(feature) + const indices = this.getAlertIndicesAlias( + authorizedRuleTypes.map((art: { id: any }) => art.id), + this.spaceId ); - const toReturn = validAuthorizedFeatures.map((feature) => { - const index = this.ruleDataService.findIndexByFeature(feature, Dataset.alerts); - if (index == null) { - throw new Error(`This feature id ${feature} should be associated to an alert index`); - } - return ( - index?.getPrimaryAlias(feature === AlertConsumers.SIEM ? this.spaceId ?? '*' : '*') ?? '' - ); - }); - return toReturn; + return indices; } catch (exc) { const errMessage = `getAuthorizedAlertsIndices failed to get authorized rule types: ${exc}`; this.logger.error(errMessage); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts index bbb33244e2975..43966d1207004 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts @@ -26,6 +26,7 @@ const alertsClientFactoryParams: AlertsClientFactoryProps = { esClient: {} as ElasticsearchClient, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; const auditLogger = auditLoggerMock.create(); @@ -53,6 +54,7 @@ describe('AlertsClientFactory', () => { esClient: {}, ruleDataService: alertsClientFactoryParams.ruleDataService, getRuleType: alertsClientFactoryParams.getRuleType, + getAlertIndicesAlias: alertsClientFactoryParams.getAlertIndicesAlias, }); }); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts index 9a171514b588a..de0afb5a0b226 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -8,7 +8,10 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, KibanaRequest, Logger } from '@kbn/core/server'; import type { RuleTypeRegistry } from '@kbn/alerting-plugin/server/types'; -import { AlertingAuthorization } from '@kbn/alerting-plugin/server'; +import { + AlertingAuthorization, + PluginStartContract as AlertingStart, +} from '@kbn/alerting-plugin/server'; import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { IRuleDataService } from '../rule_data_plugin_service'; import { AlertsClient } from './alerts_client'; @@ -20,6 +23,7 @@ export interface AlertsClientFactoryProps { securityPluginSetup: SecurityPluginSetup | undefined; ruleDataService: IRuleDataService | null; getRuleType: RuleTypeRegistry['get']; + getAlertIndicesAlias: AlertingStart['getAlertIndicesAlias']; } export class AlertsClientFactory { @@ -32,6 +36,7 @@ export class AlertsClientFactory { private securityPluginSetup!: SecurityPluginSetup | undefined; private ruleDataService!: IRuleDataService | null; private getRuleType!: RuleTypeRegistry['get']; + private getAlertIndicesAlias!: AlertingStart['getAlertIndicesAlias']; public initialize(options: AlertsClientFactoryProps) { /** @@ -48,6 +53,7 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.ruleDataService = options.ruleDataService; this.getRuleType = options.getRuleType; + this.getAlertIndicesAlias = options.getAlertIndicesAlias; } public async create(request: KibanaRequest): Promise { @@ -60,6 +66,7 @@ export class AlertsClientFactory { esClient: this.esClient, ruleDataService: this.ruleDataService!, getRuleType: this.getRuleType, + getAlertIndicesAlias: this.getAlertIndicesAlias, }); } } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index fb3acbd5e26fd..4229ae23793fc 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked = { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; const DEFAULT_SPACE = 'test_default_space_id'; @@ -334,10 +335,10 @@ describe('bulkUpdate()', () => { status: 'closed', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: active\\" and operation update - Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: active\\" and operation update - Error: Error: Unauthorized for fake.rule and apm" - `); + "queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: active\\" and operation update + Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: active\\" and operation update + Error: Error: Unauthorized for fake.rule and apm" + `); expect(auditLogger.log).toHaveBeenNthCalledWith(1, { message: `Failed attempt to update alert [id=${fakeAlertId}]`, @@ -401,10 +402,10 @@ describe('bulkUpdate()', () => { status: 'closed', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: active\\" and operation update - Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: active\\" and operation update - Error: Error: Unauthorized for fake.rule and apm" - `); + "queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: active\\" and operation update + Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: active\\" and operation update + Error: Error: Unauthorized for fake.rule and apm" + `); expect(auditLogger.log).toHaveBeenCalledTimes(2); expect(auditLogger.log).toHaveBeenNthCalledWith(1, { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update_cases.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update_cases.test.ts index 20db7747fc898..4047a3ecadd27 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update_cases.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update_cases.test.ts @@ -37,6 +37,7 @@ describe('bulkUpdateCases', () => { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts index b287ba863e5fa..37ad46a523a70 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts @@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked = { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; const DEFAULT_SPACE = 'test_default_space_id'; @@ -420,9 +421,9 @@ describe('find()', () => { index: '.alerts-observability.apm.alerts', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find - Error: Error: Unauthorized for fake.rule and apm" - `); + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: Unauthorized for fake.rule and apm" + `); expect(auditLogger.log).toHaveBeenNthCalledWith(1, { message: `Failed attempt to access alert [id=${fakeAlertId}]`, @@ -450,9 +451,9 @@ describe('find()', () => { index: '.alerts-observability.apm.alerts', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find - Error: Error: something went wrong" - `); + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: something went wrong" + `); }); describe('authorization', () => { 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 c7b342f0fa548..fb1e0eef432ef 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 @@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked = { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; const DEFAULT_SPACE = 'test_default_space_id'; @@ -266,9 +267,9 @@ describe('get()', () => { await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability.apm.alerts' })) .rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get - Error: Error: Unauthorized for fake.rule and apm" - `); + "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get + Error: Error: Unauthorized for fake.rule and apm" + `); expect(auditLogger.log).toHaveBeenNthCalledWith(1, { message: `Failed attempt to access alert [id=${fakeAlertId}]`, @@ -293,9 +294,9 @@ describe('get()', () => { await expect( alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability.apm.alerts' }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get - Error: Error: something went wrong" - `); + "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get + Error: Error: something went wrong" + `); }); describe('authorization', () => { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/remove_cases_from_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/remove_cases_from_alerts.test.ts index 08f0c3c21ea37..2611200afd85f 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/remove_cases_from_alerts.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/remove_cases_from_alerts.test.ts @@ -32,6 +32,7 @@ describe('remove cases from alerts', () => { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; beforeEach(() => { @@ -89,6 +90,7 @@ describe('remove cases from alerts', () => { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; beforeEach(() => { 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 4db890d93b326..bca5e7d967f3a 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 @@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked = { auditLogger, ruleDataService: ruleDataServiceMock.create(), getRuleType: jest.fn(), + getAlertIndicesAlias: jest.fn(), }; const DEFAULT_SPACE = 'test_default_space_id'; @@ -257,9 +258,9 @@ describe('update()', () => { index: '.alerts-observability.apm.alerts', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation update - Error: Error: Unauthorized for fake.rule and apm" - `); + "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation update + Error: Error: Unauthorized for fake.rule and apm" + `); expect(auditLogger.log).toHaveBeenNthCalledWith(1, { message: `Failed attempt to update alert [id=${fakeAlertId}]`, @@ -289,9 +290,9 @@ describe('update()', () => { index: '.alerts-observability.apm.alerts', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation update - Error: Error: something went wrong on update" - `); + "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation update + Error: Error: something went wrong on update" + `); }); test(`throws an error if ES client update fails`, async () => { diff --git a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts index 35bab55c7c4e3..e1524b99f88d9 100644 --- a/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts +++ b/x-pack/plugins/rule_registry/server/lib/get_authz_filter.ts @@ -19,7 +19,8 @@ import { export async function getAuthzFilter( authorization: PublicMethodsOf, - operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find + operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find, + featuresIds?: Set ) { const { filter } = await authorization.getAuthorizationFilter( AlertingAuthorizationEntity.Alert, @@ -27,7 +28,8 @@ export async function getAuthzFilter( type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, }, - operation + operation, + featuresIds ); return filter; } diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 0b17e237057e6..6fba837a10c1a 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -166,6 +166,7 @@ export class RuleRegistryPlugin securityPluginSetup: security, ruleDataService, getRuleType: plugins.alerting.getType, + getAlertIndicesAlias: plugins.alerting.getAlertIndicesAlias, }); const getRacClientWithRequest = (request: KibanaRequest) => { diff --git a/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts index 3441a8571e81f..259ca03478745 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_browser_fields_by_feature_id.ts @@ -35,11 +35,13 @@ export const getBrowserFieldsByFeatureId = (router: IRouter fId !== 'siem' ); const o11yIndices = - indices?.filter((index) => index.startsWith('.alerts-observability')) ?? []; + (onlyO11yFeatureIds + ? await alertsClient.getAuthorizedAlertsIndices(onlyO11yFeatureIds) + : []) ?? []; if (o11yIndices.length === 0) { return response.notFound({ body: { diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index 76c1168d50d0b..f8da02ad0666d 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -72,20 +72,18 @@ export const ruleRegistrySearchStrategyProvider = ( alerting.getAlertingAuthorizationWithRequest(deps.request), ]); let authzFilter; - - if (!siemRequest) { + const fIds = new Set(featureIds); + if (!siemRequest && featureIds.length > 0) { authzFilter = (await getAuthzFilter( authorization, - ReadOperations.Find + ReadOperations.Find, + fIds )) as estypes.QueryDslQueryContainer; } const authorizedRuleTypes = featureIds.length > 0 - ? await authorization.getAuthorizedRuleTypes( - AlertingAuthorizationEntity.Alert, - new Set(featureIds) - ) + ? await authorization.getAuthorizedRuleTypes(AlertingAuthorizationEntity.Alert, fIds) : []; return { space, authzFilter, authorizedRuleTypes }; }; diff --git a/x-pack/plugins/session_view/server/routes/alerts_client_mock.test.ts b/x-pack/plugins/session_view/server/routes/alerts_client_mock.test.ts index 282d4297d3670..10f1c48977c34 100644 --- a/x-pack/plugins/session_view/server/routes/alerts_client_mock.test.ts +++ b/x-pack/plugins/session_view/server/routes/alerts_client_mock.test.ts @@ -57,6 +57,7 @@ const getResponse = async () => { }; const esClientMock = elasticsearchServiceMock.createElasticsearchClient(getResponse()); +const getAlertIndicesAliasMock = jest.fn(); const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), authorization: alertingAuthMock, @@ -64,6 +65,7 @@ const alertsClientParams: jest.Mocked = { ruleDataService: ruleDataServiceMock.create(), esClient: esClientMock, getRuleType: jest.fn(), + getAlertIndicesAlias: getAlertIndicesAliasMock, }; export function getAlertsClientMockInstance(esClient?: ElasticsearchClient) { @@ -86,6 +88,20 @@ export function resetAlertingAuthMock() { authorizedRuleTypes.add({ producer: 'apm' }); return Promise.resolve({ authorizedRuleTypes }); }); + // @ts-expect-error + alertingAuthMock.getAuthorizedRuleTypes.mockImplementation(async () => { + const authorizedRuleTypes = [ + { + producer: 'apm', + id: 'apm.error_rate', + alerts: { + context: 'observability.apm', + }, + }, + ]; + return Promise.resolve(authorizedRuleTypes); + }); + getAlertIndicesAliasMock.mockReturnValue(['.alerts-observability.apm-default']); alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 48e5a8b8d65fd..60537366f26cb 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -11,7 +11,6 @@ export { ComparatorFnNames, getHumanReadableComparator, } from './comparator'; -export { STACK_ALERTS_FEATURE_ID, ES_QUERY_ID } from './constants'; export type { EsqlTable } from './esql_query_utils'; export { rowToDocument, transformDatatableToEsqlTable, toEsQueryHits } from './esql_query_utils'; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx index 1cc45801ffe65..fee5e7149db02 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx @@ -28,9 +28,9 @@ import { isGroupAggregation, parseAggregationResults, } from '@kbn/triggers-actions-ui-plugin/public/common'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { getComparatorScript } from '../../../../common'; import { Comparator } from '../../../../common/comparator_types'; -import { STACK_ALERTS_FEATURE_ID } from '../../../../common'; import { CommonRuleParams, EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types'; import { DEFAULT_VALUES } from '../constants'; import { DataViewSelectPopover } from '../../components/data_view_select_popover'; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx index d49ee6c15b83a..80360f113d9d4 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx @@ -21,7 +21,7 @@ import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; import type { CoreStart } from '@kbn/core/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; -import { STACK_ALERTS_FEATURE_ID } from '../../../../common/constants'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; import { EntityIndexExpression } from './expressions/entity_index_expression'; diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 7392ee2e5eb9e..1a54134d2cdc1 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -9,10 +9,10 @@ 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 { ES_QUERY_ID as ElasticsearchQuery } from '@kbn/rule-data-utils'; import { ID as IndexThreshold } from './rule_types/index_threshold/rule_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './rule_types/geo_containment'; -import { ES_QUERY_ID as ElasticsearchQuery } from './rule_types/es_query/constants'; -import { STACK_ALERTS_FEATURE_ID } from '../common'; const TransformHealth = TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts index 9627249c2e6e1..598fe60f58c84 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/constants.ts @@ -7,4 +7,3 @@ export const ActionGroupId = 'query matched'; export const ConditionMetAlertInstanceId = 'query matched'; -export { ES_QUERY_ID } from '../../../common'; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts index 8c44c6e673ad2..f44ad3f470106 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts @@ -12,10 +12,10 @@ import { parseAggregationResults, } from '@kbn/triggers-actions-ui-plugin/common'; import { isGroupAggregation } from '@kbn/triggers-actions-ui-plugin/common'; +import { ES_QUERY_ID } from '@kbn/rule-data-utils'; import { getComparatorScript } from '../../../../common'; import { OnlyEsQueryRuleParams } from '../types'; import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; -import { ES_QUERY_ID } from '../constants'; import { getSearchParams } from './get_search_params'; export interface FetchEsQueryOpts { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts index eabe7bf346669..14cd64c36dd0c 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/server'; import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { IRuleTypeAlerts } from '@kbn/alerting-plugin/server'; -import { ALERT_EVALUATION_VALUE } from '@kbn/rule-data-utils'; +import { ALERT_EVALUATION_VALUE, ES_QUERY_ID, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { StackAlert } from '@kbn/alerts-as-data-utils'; import { STACK_AAD_INDEX_NAME } from '..'; import { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS } from './fields'; @@ -21,9 +21,8 @@ import { EsQueryRuleParamsSchema, EsQueryRuleState, } from './rule_type_params'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ExecutorOptions } from './types'; -import { ActionGroupId, ES_QUERY_ID } from './constants'; +import { ActionGroupId } from './constants'; import { executor } from './executor'; import { isSearchSourceRule } from './util'; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts index 938edad8cb394..f6d1668151843 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { SavedObjectReference } from '@kbn/core/server'; import { RuleParamsAndRefs } from '@kbn/alerting-plugin/server'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import type { GeoContainmentRuleType, GeoContainmentExtractedRuleParams, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts index 4b9a7e69ac31c..ab56c5775fda3 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts @@ -11,15 +11,11 @@ import { TIME_SERIES_BUCKET_SELECTOR_FIELD, } from '@kbn/triggers-actions-ui-plugin/server'; import { isGroupAggregation } from '@kbn/triggers-actions-ui-plugin/common'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { RuleType, RuleExecutorOptions, StackAlertsStartDeps } from '../../types'; import { Params, ParamsSchema } from './rule_type_params'; import { ActionContext, BaseActionContext, addMessages } from './action_context'; -import { - ComparatorFns, - getComparatorScript, - getHumanReadableComparator, - STACK_ALERTS_FEATURE_ID, -} from '../../../common'; +import { ComparatorFns, getComparatorScript, getHumanReadableComparator } from '../../../common'; export const ID = '.index-threshold'; export const ActionGroupId = 'threshold met'; diff --git a/x-pack/plugins/synthetics/server/feature.ts b/x-pack/plugins/synthetics/server/feature.ts index 576b48b4e87ec..d8e82ea031707 100644 --- a/x-pack/plugins/synthetics/server/feature.ts +++ b/x-pack/plugins/synthetics/server/feature.ts @@ -6,8 +6,8 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { ES_QUERY_ID } from '@kbn/stack-alerts-plugin/common'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { ES_QUERY_ID } from '@kbn/rule-data-utils'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index c140e4053e4a9..ff288ec3cee97 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -77,7 +77,6 @@ "@kbn/core-saved-objects-server-mocks", "@kbn/shared-ux-page-kibana-template", "@kbn/observability-ai-assistant-plugin", - "@kbn/stack-alerts-plugin", "@kbn/unified-doc-viewer-plugin", "@kbn/discover-utils", ], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 98882b52b43ad..bf4cafcf63da9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ES_QUERY_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; export { BASE_ALERTING_API_PATH, INTERNAL_BASE_ALERTING_API_PATH, @@ -14,9 +15,6 @@ export { BASE_ACTION_API_PATH, INTERNAL_BASE_ACTION_API_PATH } from '@kbn/action export type Section = 'connectors' | 'rules' | 'alerts' | 'logs'; -export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; -export const ES_QUERY_RULE_TYPE_ID = '.es-query'; - export const routeToHome = `/`; export const routeToConnectors = `/connectors`; export const routeToRules = `/rules`; @@ -122,7 +120,4 @@ export const GLOBAL_CONNECTOR_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [ ...CONNECTOR_LOCKED_COLUMNS, ]; -export const MULTI_CONSUMER_RULE_TYPE_IDS = [ - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - ES_QUERY_RULE_TYPE_ID, -]; +export const MULTI_CONSUMER_RULE_TYPE_IDS = [OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, ES_QUERY_ID]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations_query.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations_query.ts index a581fea1832b7..b7f5be0325729 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations_query.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations_query.ts @@ -25,11 +25,12 @@ const initializeAggregationResult = (values: readonly string[]) => { interface UseLoadRuleAggregationsQueryProps { filters: RulesListFilters; enabled: boolean; + filterConsumers?: string[]; refresh?: Date; } export const useLoadRuleAggregationsQuery = (props: UseLoadRuleAggregationsQueryProps) => { - const { filters, enabled, refresh } = props; + const { filters, enabled, refresh, filterConsumers } = props; const { http, @@ -46,6 +47,7 @@ export const useLoadRuleAggregationsQuery = (props: UseLoadRuleAggregationsQuery ruleLastRunOutcomesFilter: filters.ruleLastRunOutcomes, ruleStatusesFilter: filters.ruleStatuses, tagsFilter: filters.tags, + filterConsumers, }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules_query.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules_query.ts index d4e045ae038af..90bfea7719b07 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules_query.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules_query.ts @@ -20,10 +20,11 @@ type UseLoadRulesQueryProps = Omit & { sort: LoadRulesProps['sort']; enabled: boolean; refresh?: Date; + filterConsumers?: string[]; }; export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => { - const { filters, page, sort, onPage, enabled, refresh } = props; + const { filterConsumers, filters, page, sort, onPage, enabled, refresh } = props; const { http, notifications: { toasts }, @@ -51,6 +52,7 @@ export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => { { refresh: refresh?.toISOString(), }, + filterConsumers, ], queryFn: () => { return loadRulesWithKueryFilter({ @@ -66,6 +68,7 @@ export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => { tagsFilter: filters.tags, kueryNode: filters.kueryNode, sort, + filterConsumers, }); }, onSuccess: (response) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 6e2465e58c2fd..45a0437717470 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -43,6 +43,7 @@ export async function loadRuleAggregations({ ruleExecutionStatusesFilter, ruleStatusesFilter, tagsFilter, + filterConsumers, }: LoadRuleAggregationsProps): Promise { const filters = mapFiltersToKql({ typesFilter, @@ -59,6 +60,7 @@ export async function loadRuleAggregations({ search: searchText, filter: filters.length ? filters.join(' and ') : undefined, default_search_operator: 'AND', + filter_consumers: filterConsumers, }), } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts index 61fff1e95a451..f2e56e8aec71b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_helpers.ts @@ -52,6 +52,7 @@ export interface LoadRuleAggregationsProps { ruleLastRunOutcomesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; tagsFilter?: string[]; + filterConsumers?: string[]; } export interface LoadRuleTagsProps { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts index 20d1fc9281b48..8348af72d1d95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts @@ -18,6 +18,7 @@ export async function loadRuleAggregationsWithKueryFilter({ ruleExecutionStatusesFilter, ruleStatusesFilter, tagsFilter, + filterConsumers, }: LoadRuleAggregationsProps): Promise { const filtersKueryNode = mapFiltersToKueryNode({ typesFilter, @@ -33,6 +34,7 @@ export async function loadRuleAggregationsWithKueryFilter({ { body: JSON.stringify({ ...(filtersKueryNode ? { filter: JSON.stringify(filtersKueryNode) } : {}), + filter_consumers: filterConsumers, default_search_operator: 'AND', }), } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts index ebab20adfb4a1..5effb3cd3c305 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_helpers.ts @@ -24,6 +24,7 @@ export interface LoadRulesProps { ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; kueryNode?: KueryNode; + filterConsumers?: string[]; } export const rewriteRulesResponseRes = (results: Array>): Rule[] => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts index ee3a798e50738..9d38716d35028 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts @@ -24,6 +24,7 @@ export async function loadRulesWithKueryFilter({ tagsFilter, sort = { field: 'name', direction: 'asc' }, kueryNode, + filterConsumers, }: LoadRulesProps): Promise<{ page: number; perPage: number; @@ -56,6 +57,7 @@ export async function loadRulesWithKueryFilter({ ...(filtersKueryNode ? { filter: JSON.stringify(filtersKueryNode) } : {}), sort_field: sort.field, sort_order: sort.direction, + filter_consumers: filterConsumers, }), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 7534b968dc981..88b2aa583d031 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -53,6 +53,7 @@ const RuleAdd = ({ metadata: initialMetadata, filteredRuleTypes, validConsumers, + useRuleProducer, ...props }: RuleAddProps) => { const onSaveHandler = onSave ?? reloadRules; @@ -199,7 +200,6 @@ const RuleAdd = ({ }; const ruleType = rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null; - const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = useMemo( () => getRuleErrors(rule as Rule, ruleType, config), [rule, ruleType, config] @@ -279,6 +279,7 @@ const RuleAdd = ({ hideInterval={hideInterval} onChangeMetaData={onChangeMetaData} setConsumer={setSelectedConsumer} + useRuleProducer={useRuleProducer} /> > { connectorFeatureId?: string; validConsumers?: RuleCreationValidConsumer[]; onChangeMetaData: (metadata: MetaData) => void; + useRuleProducer?: boolean; } export const RuleForm = ({ @@ -162,6 +163,7 @@ export const RuleForm = ({ connectorFeatureId = AlertingConnectorFeatureId, validConsumers, onChangeMetaData, + useRuleProducer, }: RuleFormProps) => { const { notifications: { toasts }, @@ -522,6 +524,13 @@ export const RuleForm = ({ if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); } + + if ( + useRuleProducer && + validConsumers?.includes(solution as RuleCreationValidConsumer) + ) { + setConsumer(solution as RuleCreationValidConsumer); + } }} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 197077daceaf3..83285163aeee3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -107,6 +107,7 @@ const RuleAdd = lazy(() => import('../../rule_form/rule_add')); const RuleEdit = lazy(() => import('../../rule_form/rule_edit')); export interface RulesListProps { + filterConsumers?: string[]; filteredRuleTypes?: string[]; lastResponseFilter?: string[]; lastRunOutcomeFilter?: string[]; @@ -146,6 +147,7 @@ const initialPercentileOptions = Object.values(Percentiles).map((percentile) => const EMPTY_ARRAY: string[] = []; export const RulesList = ({ + filterConsumers, filteredRuleTypes = EMPTY_ARRAY, lastResponseFilter, lastRunOutcomeFilter, @@ -263,6 +265,7 @@ export const RulesList = ({ // Fetch rules const { rulesState, loadRules, hasData, lastUpdate } = useLoadRulesQuery({ + filterConsumers, filters: computedFilter, hasDefaultRuleTypesFiltersOn, page, @@ -275,6 +278,7 @@ export const RulesList = ({ // Fetch status aggregation const { loadRuleAggregations, rulesStatusesTotal, rulesLastRunOutcomesTotal } = useLoadRuleAggregationsQuery({ + filterConsumers, filters: computedFilter, enabled: canLoadRules, refresh, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2072765072ccf..48f99f765df49 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -26,7 +26,7 @@ import type { EuiSuperSelectOption, EuiDataGridOnColumnResizeHandler, } from '@elastic/eui'; -import type { AlertConsumers } from '@kbn/rule-data-utils'; +import type { AlertConsumers, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { HttpSetup } from '@kbn/core/public'; import { KueryNode } from '@kbn/es-query'; @@ -459,6 +459,7 @@ export interface RuleAddProps> { ruleTypeIndex?: RuleTypeIndex; filteredRuleTypes?: string[]; validConsumers?: RuleCreationValidConsumer[]; + useRuleProducer?: boolean; } export interface RuleDefinitionProps { rule: Rule; @@ -828,4 +829,4 @@ export type RuleCreationValidConsumer = | typeof AlertConsumers.APM | typeof AlertConsumers.UPTIME | typeof AlertConsumers.SLO - | 'stackAlerts'; + | typeof STACK_ALERTS_FEATURE_ID; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts index e636388fb269a..11da73c6cb3f3 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_fired.ts @@ -9,7 +9,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; import { waitForAlertInIndex, waitForRuleStatus } from '../helpers/alerting_wait_for_helpers'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts index 8bea4a377ea03..1bcbd5bbff89e 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_pct_no_data.ts @@ -8,7 +8,7 @@ import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts index 9ff58b68db98b..133ff70a3c32a 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/avg_us_fired.ts @@ -11,7 +11,7 @@ import { format } from 'url'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index 0d9c1970bd8c4..afc125e85f632 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -15,7 +15,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; import { waitForAlertInIndex, waitForRuleStatus } from '../helpers/alerting_wait_for_helpers'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts index 72e1866f1d953..911ee13bd60af 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/documents_count_fired.ts @@ -9,7 +9,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; import { waitForAlertInIndex, waitForRuleStatus } from '../helpers/alerting_wait_for_helpers'; diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts index 91ae5d43bdb09..0d7e44c780699 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule/group_by_fired.ts @@ -16,7 +16,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; import { createDataView, deleteDataView } from '../helpers/data_view'; import { diff --git a/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts b/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts index 71b0570d21c90..6b77905c5adcb 100644 --- a/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts +++ b/x-pack/test/alerting_api_integration/observability/threshold_rule_data_view.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../common/ftr_provider_context'; import { getUrlPrefix, ObjectRemover } from '../common/lib'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts index ce30fbc283034..8cca92320cfa7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts @@ -150,7 +150,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(failedUpdateKeyDueToAlertsPrivilegesResponse.body).to.eql({ error: 'Forbidden', message: - 'Unauthorized to updateApiKey a "test.always-firing" rule for "alertsFixture"', + 'Unauthorized by "alertsFixture" to updateApiKey "test.always-firing" rule', statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts index 471faeda4c25b..0366eca2ad24d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts @@ -221,7 +221,6 @@ export default ({ getService }: FtrProviderContext) => { expect(response.statusCode).to.eql(400); break; case 'superuser at space1': - expect(response.body).to.eql(defaultSuccessfulResponse); expect(response.statusCode).to.eql(200); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts index ae2e2d61267c4..8f9b7566b7640 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts @@ -137,6 +137,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { hasFieldsForAAD: false, hasAlertsMappings: false, ruleTaskTimeout: '5m', + validLegacyConsumers: ['alerts'], }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); diff --git a/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/data.json new file mode 100644 index 0000000000000..c3a19cfbcf81a --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/data.json @@ -0,0 +1,104 @@ +{ + "type": "doc", + "value": { + "index": ".alerts-observability.apm.alerts-default-000001", + "id": "NoxgpHkBqbdrfX07MqXV", + "source": { + "event.kind" : "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "message": "hello world 1", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.workflow_status": "open", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z" + }, + "kibana.alert.status": "active", + "kibana.space_ids": ["space1", "space2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.apm.alerts-default-000001", + "id": "space1alert", + "source": { + "event.kind" : "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "message": "hello world 1", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.workflow_status": "recovered", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z", + "lte": "2020-12-16T15:16:19.570Z" + }, + "kibana.alert.status": "recovered", + "kibana.space_ids": ["space1"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.apm.alerts-default-000001", + "id": "space2alert", + "source": { + "event.kind" : "signal", + "@timestamp": "2020-12-16T15:16:19.570Z", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "message": "hello world 1", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.workflow_status": "open", + "kibana.alert.status": "active", + "kibana.space_ids": ["space2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.logs.alerts-default-000001", + "id": "123456789XYZ", + "source": { + "event.kind": "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "logs.alert.document.count", + "message": "hello world 1", + "kibana.alert.rule.consumer": "logs", + "kibana.alert.workflow_status": "open", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z" + }, + "kibana.alert.status": "active", + "kibana.space_ids": ["space1", "space2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.logs.alerts-default-000001", + "id": "space1alertLogs", + "source": { + "event.kind": "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "logs.alert.document.count", + "message": "hello world 1", + "kibana.alert.rule.consumer": "logs", + "kibana.alert.workflow_status": "recovered", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z", + "lte": "2020-12-16T15:27:19.570Z" + }, + "kibana.alert.end": "2020-12-16T15:27:19.570Z", + "kibana.alert.status": "recovered", + "kibana.space_ids": ["space1"] + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/mappings.json new file mode 100644 index 0000000000000..0faf5daf3df76 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/o11y_alerts/mappings.json @@ -0,0 +1,66 @@ +{ + "type": "index", + "value": { + "index": ".alerts-observability.apm.alerts-default-000001", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.alert.rule.consumer": { + "type": "keyword", + "ignore_above": 256 + }, + "kibana.alert.time_range": { + "type": "date_range", + "format": "epoch_millis||strict_date_optional_time" + } + } + } + } +} + + +{ + "type": "index", + "value": { + "index": ".alerts-observability.logs.alerts-default-000001", + "aliases": { + ".alerts-observability.logs.alerts-default": {} + }, + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.alert.rule.consumer": { + "type": "keyword", + "ignore_above": 256 + }, + "kibana.alert.status": { + "type": "keyword", + "ignore_above": 256 + }, + "kibana.alert.time_range": { + "type": "date_range", + "format": "epoch_millis||strict_date_optional_time" + }, + "kibana.alert.end": { + "type": "date" + } + } + } + } +} diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts index b22fc830cb73d..5da72c7f6a76a 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts @@ -24,11 +24,11 @@ export default ({ getService }: FtrProviderContext) => { describe('Alerts - GET - _alert_summary', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/o11y_alerts'); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/o11y_alerts'); }); it('Alert summary for all LOGS alerts with features', async () => { 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 index 2ac420a8beb6b..1146fa925908c 100644 --- 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 @@ -20,7 +20,7 @@ export default ({ getService }: FtrProviderContext) => { const TEST_URL = '/internal/rac/alerts'; const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; - const APM_ALERT_INDEX = '.alerts-observability.apm.alerts'; + const APM_ALERT_INDEX = '.alerts-observability.apm.alerts-default'; const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; const getAPMIndexName = async (user: User, space: string, expectedStatusCode: number = 200) => { @@ -53,12 +53,12 @@ export default ({ getService }: FtrProviderContext) => { describe('Users:', () => { it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1); - expect(indexNames.includes(`${APM_ALERT_INDEX}-*`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(APM_ALERT_INDEX)).to.eql(true); // 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); - expect(indexNames.includes(`${APM_ALERT_INDEX}-*`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(APM_ALERT_INDEX)).to.eql(true); // 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 () => { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts index feb84ab625fdb..94cbdbce77491 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext) => { 'uptime', ]); expect(Object.keys(resp.browserFields)).toEqual( - expect.arrayContaining(['base', 'event', 'kibana', 'message']) + expect.arrayContaining(['base', 'event', 'kibana']) ); }); @@ -66,7 +66,6 @@ export default ({ getService }: FtrProviderContext) => { 'error', 'event', 'kibana', - 'message', 'monitor', 'observer', 'tls', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts index 27c8471c4eff0..4b13bf57e4f45 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts @@ -9,7 +9,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts index 8050f0a672cb7..413f810610cac 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts @@ -8,7 +8,7 @@ import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index 2fab6c3a9ebe2..c2503c10e3488 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -15,7 +15,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts index 1d79f3aca06a7..f311fd4b902fc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts @@ -9,7 +9,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts index b8b0418a2124e..e17209c044656 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts @@ -17,7 +17,7 @@ import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/threshold/threshold_executor'; import expect from '@kbn/expect'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index de711146da7ec..323c9271d1e9e 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -53,6 +53,7 @@ "@kbn/data-plugin", "@kbn/dev-utils", "@kbn/bfetch-plugin", + "@kbn/rule-data-utils", "@kbn/rison", ] } From 38ab867dd7cbe751b5ca07f9604c6ee0b348e148 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Sep 2023 23:07:32 +0000 Subject: [PATCH 03/15] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/stack_alerts/tsconfig.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 08a4f0ca99e9c..1119b8ca8776b 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -32,12 +32,6 @@ "@kbn/i18n-react", "@kbn/charts-plugin", "@kbn/es-ui-shared-plugin", - "@kbn/core-http-browser", - "@kbn/core-doc-links-browser", - "@kbn/core-ui-settings-server", - "@kbn/kibana-utils-plugin", - "@kbn/usage-collection-plugin", - "@kbn/react-field", "@kbn/core-elasticsearch-server-mocks", "@kbn/logging-mocks", "@kbn/share-plugin", From 3e3234b39772dd981b6514ecbfea20fdf41b9e7c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Thu, 7 Sep 2023 19:22:56 -0400 Subject: [PATCH 04/15] messed up merge --- .../rule_types/geo_containment/index.ts | 26 ++ .../rule_types/geo_containment/readme.md | 121 ++++++++ .../rule_form/boundary_form.test.tsx | 92 ++++++ .../rule_form/boundary_form.tsx | 233 +++++++++++++++ .../rule_form/data_view_select.tsx | 58 ++++ .../rule_form/entity_form.test.tsx | 95 ++++++ .../geo_containment/rule_form/entity_form.tsx | 277 ++++++++++++++++++ .../geo_containment/rule_form/index.ts | 11 + .../geo_containment/rule_form/query_input.tsx | 98 +++++++ .../geo_containment/rule_form/rule_form.tsx | 63 ++++ .../rule_form/single_field_select.tsx | 86 ++++++ .../rule_types/geo_containment/types.ts | 24 ++ .../geo_containment/validation.test.ts | 145 +++++++++ .../rule_types/geo_containment/validation.ts | 86 ++++++ 14 files changed, 1415 insertions(+) create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/index.ts create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/readme.md create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts create mode 100644 x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/index.ts new file mode 100644 index 0000000000000..7921471550f37 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import { validateExpression } from './validation'; +import { GeoContainmentAlertParams } from './types'; + +export function getRuleType(): RuleTypeModel { + return { + id: '.geo-containment', + description: i18n.translate('xpack.stackAlerts.geoContainment.descriptionText', { + defaultMessage: 'Alert when an entity is contained or no longer contained within a boundary.', + }), + iconClass: 'globe', + documentationUrl: null, + ruleParamsExpression: lazy(() => import('./rule_form')), + validate: validateExpression, + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/readme.md b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/readme.md new file mode 100644 index 0000000000000..0ee0c19f0d432 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/readme.md @@ -0,0 +1,121 @@ +## Instructions for loading & observing data + +There are several steps required to set up geo containment alerts for testing in a way +that allows you to view triggered alerts as they happen. These instructions outline +how to load test data, but really these steps can be used to load any data for geo +containment alerts so long as you have the following data: +- An index containing a`geo_point` or `geo_shape` field and a `date` field. This data is presumed to +be dynamic (updated). +- An index containing `geo_shape` data, such as boundary data, bounding box data, etc. +This data is presumed to be static (not updated). Shape data matching the query is +harvested once when the alert is created and anytime after when alert is re-enabled +after disablement +The ability for containment alerts to monitor data requires there be somewhat "real time" +data streaming in as indicated by the `date` field. + +### 1. Set experimental flag to enable containment alerts +- Your `kibana.yml` config file is located in the `config/` dir in the base of your kibana +project. To edit it, open this file in your editor of choice, add the line described in +the next step to the bottom of the file (or really anywhere) and save. For more details +on different config modifications or on how to make production config modifications, +see [the current docs](https://www.elastic.co/guide/en/kibana/current/settings.html) + +### 2. Run ES/Kibana dev env with ssl enabled +- In two terminals, run the normal commands to launch both elasticsearch and kibana but +append `--ssl` to the end of each as an arg, i.e.: + - `yarn es snapshot --ssl # Runs Elasticsearch` + - `yarn start --ssl # Runs Kibana` + +### 3. Get an MTA data api key +- You'll need to obtain an NYC MTA api key, you can request this + key [here](https://docs.google.com/forms/d/e/1FAIpQLSfGUZA6h4eHd2-ImaK5Q_I5Gb7C3UEP5vYDALyGd7r3h08YKg/viewform?hl=en&formkey=dG9kcGIxRFpSS0NhQWM4UjA0V0VkNGc6MQ#gid=0) + +### 4. Get trackable point data (MTA bus data) into elasticsearch +- You'll be using the script: `https://github.com/thomasneirynck/mtatracks` to harvest +live bus data to populate the system. Clone the repo and follow the instructions in +the readme to set up. +- Using the MTA key you obtained in the previous step, the final command to run +in a local terminal should look something like the following. This script loads large +quantities of data the frequency listed below (20000ms = 20s) or higher: +`node ./load_tracks.js -a -f 20000` + +### 5. Open required Kibana tabs +There are 3 separate tabs you'll need for a combination of loading and viewing the +data. Since you'll be jumping between them, it might be easiest to just open them +upfront. Each is preceded by `https://localhost:5601//app/`: +- Stack Management > Data Views: `management/kibana/dataViews` +- Stack Management > Alerts & Actions: `management/insightsAndAlerting/triggersActions/alerts` +- Maps: `maps` + +### 6 Create map to monitor alerts +- Go to the Maps app and create a new map +- Using GeoJSON Upload, upload the GeoJSON file located in the folder of the previously +cloned `mta_tracks` repo: `nyc-neighborhoods.geo.json`. Accept all of the default +settings and add the layer. +- You may want to click your newly added layer and select "Fit to data" so you can see the +boundaries you've added. +_ When finished uploading and adding the layer, save the map using a name of your +choice. +- Keep the Maps tab open, you'll come back to this + +### 7. Create data view for generated tracks +- Go to the data view tab to create a new data view. +- Give it the index name `mtatracks*` +- For `Time field` select `@timestamp` +- Click `Create data view` +- Leave this tab open, you'll come back to this + +### 8. Create containment alert +- Go to the Alerts tab and click `Create Alert` > `Tracking containment` +- Fill the side bar form top to bottom. This _should_ flow somewhat logically. In the top +section, set both `Check every` and `Notify every` to `1 minute`. + For `Notify`, leave +on default selected option `Only on status change`, this will notify only on newly +contained entities. + **Please note that `2 seconds` is an unusually quick interval but done here for demo + purposes. With real world data, setting an appropriate interval speed is highly dependent + upon the quantity, update frequency and complexity of data handled.** +- The default settings for `Select Entity` will mostly be correct. Select `mta_tracks*` +as the index you'd like to track. Use the defaults populated under +`Select entity` > `INDEX`, update `Select entity` > `BY` to `vehicle_ref`. +- For `Select boundary` > `INDEX`, select `nyc-neighborhoods` and all populated defaults. +- Under `Actions`, create an `Server log` action, then create a `Connector` which you can simply name +`Log test`. +- For `Run when`, the default `Tracking containment met` will work here. This will track +only points that are newly contained in the boundaries. +- Leave the log level at `Info` +- For the message, use the following sample message or one of your own: +``` +Entity: {{context.entityId}} with document ID: {{context.entityDocumentId}} has been recorded at location: {{context.entityLocation}} in boundary: {{context.containingBoundaryName}}({{context.containingBoundaryId}}) at {{context.entityDateTime}}. This was detected by the alerting framework at: {{context.detectionDateTime}}. +``` +- At the bottom right, click `Save`. Your alert should now be created! +- You should now be able to see alerts generated in your Kibana console log. + +### 9. Visually confirm your alerts with Maps +- Creating layers + - Using the source data below, you can create the following layers: + - Boundary data (`nyc-neighborhoods`) + - Boundary layer + - Original tracks data (`mtatracks*`) + - Last known location + - Geo-line track + - Boundary layer + - This layer should already be added from when you uploaded the GeoJSON + file earlier. If it's not already added, it can be added by selecting `Documents` + > `Data views` > `nyc-neighborhoods` then accept the defaults and add the layer. + - Vehicle tracks + - Add `Tracks` > `Data views` > `mtatracks*`, accept the defaults selected and set `Entity` > `entity_id`. Add the layer and style appropriately. + - Last known location + - Add `Documents` > `Data views` > `mtatracks*` and select `Show top hits per entity` + - For `Entity` select `entity_id` and add the layer. + - The only required setting on the following screen is to set `Sorting` to sort on `@timestamp` +- Update time scope of data + - Changing the refresh rate `Refresh every`: `4 seconds` keeps the layers updated and in particular + shows the latest values obtained in the `Top hits` layer + - The time picker should already be set to the default `15 minutes`, this is a good default but + can be adjusted up or down to see more or less data respectively +- General tips + - Style layers with contrasting colors to clearly see each + - Consider using icons for the `Top hits` vehicle movement layer + - Consider adding tooltips to layers to better understand the data in your layers. + - Save your Map anytime you've made any layer adjustments diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx new file mode 100644 index 0000000000000..971d8e24df7d9 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 React from 'react'; +import { act } from 'react-dom/test-utils'; +import { BoundaryForm } from './boundary_form'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +jest.mock('./query_input', () => { + return { + QueryInput: () =>
mock query input
, + }; +}); + +test('should not call prop callbacks on render', async () => { + const DATA_VIEW_TITLE = 'my-boundaries*'; + const DATA_VIEW_ID = '1234'; + const mockDataView = { + id: DATA_VIEW_ID, + fields: [ + { + name: 'location', + type: 'geo_shape', + }, + ], + title: DATA_VIEW_TITLE, + }; + const props = { + data: { + indexPatterns: { + get: async () => mockDataView, + }, + } as unknown as DataPublicPluginStart, + getValidationError: () => null, + ruleParams: { + boundaryIndexTitle: DATA_VIEW_TITLE, + boundaryIndexId: DATA_VIEW_ID, + boundaryGeoField: 'location', + boundaryNameField: 'name', + boundaryIndexQuery: { + query: 'population > 1000', + language: 'kuery', + }, + } as unknown as GeoContainmentAlertParams, + setDataViewId: jest.fn(), + setDataViewTitle: jest.fn(), + setGeoField: jest.fn(), + setNameField: jest.fn(), + setQuery: jest.fn(), + unifiedSearch: { + ui: { + IndexPatternSelect: () => { + return '
mock IndexPatternSelect
'; + }, + }, + } as unknown as UnifiedSearchPublicPluginStart, + }; + + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Assert that geospatial dataView fields are loaded + // to ensure test is properly awaiting async useEffect + let geoFieldsLoaded = false; + wrapper.findWhere((n) => { + if ( + n.name() === 'SingleFieldSelect' && + n.props().value === 'location' && + n.props().fields.length === 1 + ) { + geoFieldsLoaded = true; + } + return false; + }); + expect(geoFieldsLoaded).toBe(true); + + expect(props.setDataViewId).not.toHaveBeenCalled(); + expect(props.setDataViewTitle).not.toHaveBeenCalled(); + expect(props.setGeoField).not.toHaveBeenCalled(); + expect(props.setNameField).not.toHaveBeenCalled(); + expect(props.setQuery).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx new file mode 100644 index 0000000000000..9674ee3a7fb02 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx @@ -0,0 +1,233 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiPanel, EuiSkeletonText, EuiSpacer, EuiTitle } from '@elastic/eui'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { Query } from '@kbn/es-query'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import { DataViewSelect } from './data_view_select'; +import { SingleFieldSelect } from './single_field_select'; +import { QueryInput } from './query_input'; + +export const BOUNDARY_GEO_FIELD_TYPES = ['geo_shape']; + +function getGeoFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => BOUNDARY_GEO_FIELD_TYPES.includes(field.type)); +} + +function getNameFields(fields: DataViewField[]) { + return fields.filter( + (field: DataViewField) => + ['string', 'number', 'ip'].includes(field.type) && + !field.name.startsWith('_') && + !field.name.endsWith('keyword') + ); +} + +interface Props { + data: DataPublicPluginStart; + getValidationError: (key: string) => string | null; + ruleParams: GeoContainmentAlertParams; + setDataViewId: (id: string) => void; + setDataViewTitle: (title: string) => void; + setGeoField: (fieldName: string) => void; + setNameField: (fieldName: string | undefined) => void; + setQuery: (query: Query) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const BoundaryForm = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const [dataView, setDataView] = useState(); + const [dataViewNotFound, setDataViewNotFound] = useState(false); + const [geoFields, setGeoFields] = useState([]); + const [nameFields, setNameFields] = useState([]); + + useEffect(() => { + if (!props.ruleParams.boundaryIndexId || props.ruleParams.boundaryIndexId === dataView?.id) { + return; + } + + let ignore = false; + setIsLoading(true); + setDataViewNotFound(false); + props.data.indexPatterns + .get(props.ruleParams.boundaryIndexId) + .then((nextDataView) => { + if (!ignore) { + setDataView(nextDataView); + setGeoFields(getGeoFields(nextDataView.fields)); + setNameFields(getNameFields(nextDataView.fields)); + setIsLoading(false); + } + }) + .catch(() => { + if (!ignore) { + setDataViewNotFound(true); + setIsLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, [props.ruleParams.boundaryIndexId, dataView?.id, props.data.indexPatterns]); + + function getDataViewError() { + const validationError = props.getValidationError('boundaryIndexTitle'); + if (validationError) { + return validationError; + } + + if (dataView && geoFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { + defaultMessage: + 'Data view does not contain geospatial fields. Must have one of type: {geoFieldTypes}.', + values: { + geoFieldTypes: BOUNDARY_GEO_FIELD_TYPES.join(', '), + }, + }); + } + + if (dataViewNotFound) { + return i18n.translate('xpack.stackAlerts.geoContainment.dataViewNotFound', { + defaultMessage: `Unable to find data view '{id}'`, + values: { id: props.ruleParams.indexId }, + }); + } + + return null; + } + + const dataViewError = getDataViewError(); + const geoFieldError = props.getValidationError('boundaryGeoField'); + + return ( + + +
+ +
+
+ + + + + + { + if (!nextDataView.id) { + return; + } + props.setDataViewId(nextDataView.id); + props.setDataViewTitle(nextDataView.title); + + const nextGeoFields = getGeoFields(nextDataView.fields); + if (nextGeoFields.length) { + props.setGeoField(nextGeoFields[0].name); + } else if ('boundaryGeoField' in props.ruleParams) { + props.setGeoField(''); + } + + // do not attempt to auto select name field + // its optional plus there can be many matches so auto selecting the correct field is improbable + if ('boundaryNameField' in props.ruleParams) { + props.setNameField(undefined); + } + }} + unifiedSearch={props.unifiedSearch} + /> + + + {props.ruleParams.boundaryIndexId && ( + <> + + { + if (fieldName) { + props.setGeoField(fieldName); + } + }} + fields={geoFields} + /> + + + + { + if (fieldName) { + props.setNameField(fieldName); + } + }} + fields={nameFields} + /> + + + + { + props.setQuery(query); + }} + query={props.ruleParams.boundaryIndexQuery} + /> + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx new file mode 100644 index 0000000000000..5b162a1fb5ff0 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx @@ -0,0 +1,58 @@ +/* + * 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 React, { useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { i18n } from '@kbn/i18n'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; + +interface Props { + data: DataPublicPluginStart; + dataViewId?: string; + isInvalid: boolean; + onChange: (dataview: DataView) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const DataViewSelect = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const isMounted = useMountedState(); + + return ( + { + if (!dataViewId) { + return; + } + try { + setIsLoading(true); + const dataView = await props.data.indexPatterns.get(dataViewId); + if (isMounted()) { + props.onChange(dataView); + setIsLoading(false); + } + } catch (error) { + // ignore indexPatterns.get error, + // if data view does not exist, select will not update rule params + if (isMounted()) { + setIsLoading(false); + } + } + }} + placeholder={i18n.translate('xpack.stackAlerts.geoContainment.dataViewSelectPlaceholder', { + defaultMessage: 'Select data view', + })} + /> + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx new file mode 100644 index 0000000000000..da0f52897dd42 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 React from 'react'; +import { act } from 'react-dom/test-utils'; +import { EntityForm } from './entity_form'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +jest.mock('./query_input', () => { + return { + QueryInput: () =>
mock query input
, + }; +}); + +test('should not call prop callbacks on render', async () => { + const DATA_VIEW_TITLE = 'my-entities*'; + const DATA_VIEW_ID = '1234'; + const mockDataView = { + id: DATA_VIEW_ID, + fields: [ + { + name: 'location', + type: 'geo_point', + }, + ], + title: DATA_VIEW_TITLE, + }; + const props = { + data: { + indexPatterns: { + get: async () => mockDataView, + }, + } as unknown as DataPublicPluginStart, + getValidationError: () => null, + ruleParams: { + index: DATA_VIEW_TITLE, + indexId: DATA_VIEW_ID, + geoField: 'location', + entity: 'entity_id', + dateField: 'time', + indexQuery: { + query: 'population > 1000', + language: 'kuery', + }, + } as unknown as GeoContainmentAlertParams, + setDataViewId: jest.fn(), + setDataViewTitle: jest.fn(), + setDateField: jest.fn(), + setEntityField: jest.fn(), + setGeoField: jest.fn(), + setQuery: jest.fn(), + unifiedSearch: { + ui: { + IndexPatternSelect: () => { + return '
mock IndexPatternSelect
'; + }, + }, + } as unknown as UnifiedSearchPublicPluginStart, + }; + + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Assert that geospatial dataView fields are loaded + // to ensure test is properly awaiting async useEffect + let geoFieldsLoaded = false; + wrapper.findWhere((n) => { + if ( + n.name() === 'SingleFieldSelect' && + n.props().value === 'location' && + n.props().fields.length === 1 + ) { + geoFieldsLoaded = true; + } + return false; + }); + expect(geoFieldsLoaded).toBe(true); + + expect(props.setDataViewId).not.toHaveBeenCalled(); + expect(props.setDataViewTitle).not.toHaveBeenCalled(); + expect(props.setDateField).not.toHaveBeenCalled(); + expect(props.setEntityField).not.toHaveBeenCalled(); + expect(props.setGeoField).not.toHaveBeenCalled(); + expect(props.setQuery).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx new file mode 100644 index 0000000000000..3923b494fa045 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx @@ -0,0 +1,277 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiPanel, EuiSkeletonText, EuiSpacer, EuiTitle } from '@elastic/eui'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { Query } from '@kbn/es-query'; +import type { GeoContainmentAlertParams } from '../types'; +import { DataViewSelect } from './data_view_select'; +import { SingleFieldSelect } from './single_field_select'; +import { QueryInput } from './query_input'; + +export const ENTITY_GEO_FIELD_TYPES = ['geo_point', 'geo_shape']; + +function getDateFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => field.type === 'date'); +} + +function getEntityFields(fields: DataViewField[]) { + return fields.filter( + (field: DataViewField) => + field.aggregatable && + ['string', 'number', 'ip'].includes(field.type) && + !field.name.startsWith('_') + ); +} + +function getGeoFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => ENTITY_GEO_FIELD_TYPES.includes(field.type)); +} + +interface Props { + data: DataPublicPluginStart; + getValidationError: (key: string) => string | null; + ruleParams: GeoContainmentAlertParams; + setDataViewId: (id: string) => void; + setDataViewTitle: (title: string) => void; + setDateField: (fieldName: string) => void; + setEntityField: (fieldName: string) => void; + setGeoField: (fieldName: string) => void; + setQuery: (query: Query) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const EntityForm = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const [dataView, setDataView] = useState(); + const [dataViewNotFound, setDataViewNotFound] = useState(false); + const [dateFields, setDateFields] = useState([]); + const [entityFields, setEntityFields] = useState([]); + const [geoFields, setGeoFields] = useState([]); + + useEffect(() => { + if (!props.ruleParams.indexId || props.ruleParams.indexId === dataView?.id) { + return; + } + + let ignore = false; + setIsLoading(true); + setDataViewNotFound(false); + props.data.indexPatterns + .get(props.ruleParams.indexId) + .then((nextDataView) => { + if (!ignore) { + setDataView(nextDataView); + setDateFields(getDateFields(nextDataView.fields)); + setEntityFields(getEntityFields(nextDataView.fields)); + setGeoFields(getGeoFields(nextDataView.fields)); + setIsLoading(false); + } + }) + .catch(() => { + if (!ignore) { + setDataViewNotFound(true); + setIsLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, [props.ruleParams.indexId, dataView?.id, props.data.indexPatterns]); + + function getDataViewError() { + const validationError = props.getValidationError('index'); + if (validationError) { + return validationError; + } + + if (dataView && dateFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noDateFieldInIndexPattern.message', { + defaultMessage: 'Data view does not contain date fields.', + }); + } + + if (dataView && geoFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { + defaultMessage: + 'Data view does not contain geospatial fields. Must have one of type: {geoFieldTypes}.', + values: { + geoFieldTypes: ENTITY_GEO_FIELD_TYPES.join(', '), + }, + }); + } + + if (dataViewNotFound) { + return i18n.translate('xpack.stackAlerts.geoContainment.dataViewNotFound', { + defaultMessage: `Unable to find data view '{id}'`, + values: { id: props.ruleParams.indexId }, + }); + } + + return null; + } + + const dataViewError = getDataViewError(); + const dateFieldError = props.getValidationError('dateField'); + const geoFieldError = props.getValidationError('geoField'); + const entityFieldError = props.getValidationError('entity'); + + return ( + + +
+ +
+
+ + + + + + { + if (!nextDataView.id) { + return; + } + props.setDataViewId(nextDataView.id); + props.setDataViewTitle(nextDataView.title); + + const nextDateFields = getDateFields(nextDataView.fields); + if (nextDateFields.length) { + props.setDateField(nextDateFields[0].name); + } else if ('dateField' in props.ruleParams) { + props.setDateField(''); + } + + // do not attempt to auto select entity field + // there can be many matches so auto selecting the correct field is improbable + if ('entity' in props.ruleParams) { + props.setEntityField(''); + } + + const nextGeoFields = getGeoFields(nextDataView.fields); + if (nextGeoFields.length) { + props.setGeoField(nextGeoFields[0].name); + } else if ('geoField' in props.ruleParams) { + props.setGeoField(''); + } + }} + unifiedSearch={props.unifiedSearch} + /> + + + {props.ruleParams.indexId && ( + <> + + { + if (fieldName) { + props.setDateField(fieldName); + } + }} + fields={dateFields} + /> + + + + { + if (fieldName) { + props.setGeoField(fieldName); + } + }} + fields={geoFields} + /> + + + + { + if (fieldName) { + props.setEntityField(fieldName); + } + }} + fields={entityFields} + /> + + + + { + props.setQuery(query); + }} + query={props.ruleParams.indexQuery} + /> + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts new file mode 100644 index 0000000000000..dba9cea07ed8d --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { RuleForm } from './rule_form'; + +// eslint-disable-next-line import/no-default-export +export default RuleForm; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx new file mode 100644 index 0000000000000..5a578276dbdd0 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx @@ -0,0 +1,98 @@ +/* + * 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 React, { useState } from 'react'; +import { QueryStringInput } from '@kbn/unified-search-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { Query } from '@kbn/es-query'; +import { fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; +import type { CoreStart } from '@kbn/core/public'; +import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; + +function validateQuery(query: Query) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + query.language === 'kuery' ? fromKueryExpression(query.query) : luceneStringToDsl(query.query); + } catch (err) { + return false; + } + return true; +} + +interface Props { + dataView?: DataView; + onChange: (query: Query) => void; + query?: Query; +} + +export const QueryInput = (props: Props) => { + const { + data, + dataViews, + docLinks, + http, + notifications, + storage, + uiSettings, + unifiedSearch, + usageCollection, + } = useKibana<{ + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; + docLinks: DocLinksStart; + http: HttpSetup; + notifications: CoreStart['notifications']; + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + unifiedSearch: UnifiedSearchPublicPluginStart; + usageCollection: UsageCollectionStart; + }>().services; + + const [localQuery, setLocalQuery] = useState( + props.query || { + query: '', + language: 'kuery', + } + ); + + return ( + { + if (query.language) { + setLocalQuery(query); + if (validateQuery(query)) { + props.onChange(query); + } + } + }} + appName={STACK_ALERTS_FEATURE_ID} + deps={{ + unifiedSearch, + notifications, + http, + docLinks, + uiSettings, + data, + dataViews, + storage, + usageCollection, + }} + /> + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx new file mode 100644 index 0000000000000..52e6baae16536 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx @@ -0,0 +1,63 @@ +/* + * 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 React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import type { Query } from '@kbn/es-query'; +import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import { BoundaryForm } from './boundary_form'; +import { EntityForm } from './entity_form'; + +export const RuleForm: React.FunctionComponent< + RuleTypeParamsExpressionProps +> = (props) => { + function getValidationError(key: string) { + return props.errors[key]?.length > 0 && key in props.ruleParams + ? (props.errors[key] as string[])[0] + : null; + } + + return ( + <> + props.setRuleParams('indexId', id)} + setDataViewTitle={(title: string) => props.setRuleParams('index', title)} + setDateField={(fieldName: string) => props.setRuleParams('dateField', fieldName)} + setEntityField={(fieldName: string) => props.setRuleParams('entity', fieldName)} + setGeoField={(fieldName: string) => props.setRuleParams('geoField', fieldName)} + setQuery={(query: Query) => props.setRuleParams('indexQuery', query)} + unifiedSearch={props.unifiedSearch} + /> + + + + { + props.setRuleParams('boundaryIndexId', id); + // TODO remove unused param 'boundaryType' + props.setRuleParams('boundaryType', 'entireIndex'); + }} + setDataViewTitle={(title: string) => props.setRuleParams('boundaryIndexTitle', title)} + setGeoField={(fieldName: string) => props.setRuleParams('boundaryGeoField', fieldName)} + setNameField={(fieldName: string | undefined) => + props.setRuleParams('boundaryNameField', fieldName) + } + setQuery={(query: Query) => props.setRuleParams('boundaryIndexQuery', query)} + unifiedSearch={props.unifiedSearch} + /> + + + + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx new file mode 100644 index 0000000000000..71ef2d301a9c7 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx @@ -0,0 +1,86 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field'; +import { DataViewField } from '@kbn/data-views-plugin/public'; + +function fieldsToOptions(fields?: DataViewField[]): Array> { + if (!fields) { + return []; + } + + return fields + .map((field) => ({ + value: field, + label: field.name, + })) + .sort((a, b) => { + return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); + }); +} + +interface Props { + isInvalid?: boolean; + placeholder: string; + value: string | null | undefined; + onChange: (fieldName?: string) => void; + fields: DataViewField[]; +} + +export function SingleFieldSelect({ isInvalid, placeholder, value, onChange, fields }: Props) { + function renderOption( + option: EuiComboBoxOptionOption, + searchValue: string, + contentClassName: string + ) { + return ( + + + + + + {option.label} + + + ); + } + + const onSelection = (selectedOptions: Array>) => { + onChange(_.get(selectedOptions, '0.value.name')); + }; + + const selectedOptions: Array> = []; + if (value && fields) { + const selectedField = fields.find((field: DataViewField) => field.name === value); + if (selectedField) { + selectedOptions.push({ value: selectedField, label: value }); + } + } + + return ( + + ); +} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts new file mode 100644 index 0000000000000..4eb717a23e437 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts @@ -0,0 +1,24 @@ +/* + * 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 { RuleTypeParams } from '@kbn/alerting-plugin/common'; +import type { Query } from '@kbn/es-query'; + +export interface GeoContainmentAlertParams extends RuleTypeParams { + index: string; + indexId: string; + geoField: string; + entity: string; + dateField: string; + boundaryType: string; + boundaryIndexTitle: string; + boundaryIndexId: string; + boundaryGeoField: string; + boundaryNameField?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; +} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts new file mode 100644 index 0000000000000..30aef903c6ef5 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { GeoContainmentAlertParams } from './types'; +import { validateExpression } from './validation'; + +describe('expression params validation', () => { + test('if index property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: '', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.index[0]).toBe('Data view is required.'); + }); + + test('if geoField property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: '', + entity: 'testField', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.geoField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.geoField[0]).toBe('Geo field is required.'); + }); + + test('if entity property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: '', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.entity.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.entity[0]).toBe('Entity is required.'); + }); + + test('if dateField property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: '', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.dateField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.dateField[0]).toBe('Date field is required.'); + }); + + test('if boundaryType property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: 'testField', + boundaryType: '', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.boundaryType.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.boundaryType[0]).toBe( + 'Boundary type is required.' + ); + }); + + test('if boundaryIndexTitle property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: '', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + }; + expect(validateExpression(initialParams).errors.boundaryIndexTitle.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.boundaryIndexTitle[0]).toBe( + 'Boundary data view is required.' + ); + }); + + test('if boundaryGeoField property is invalid should return proper error message', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: '', + }; + expect(validateExpression(initialParams).errors.boundaryGeoField.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.boundaryGeoField[0]).toBe( + 'Boundary geo field is required.' + ); + }); + + test('if boundaryNameField property is missing should not return error', () => { + const initialParams: GeoContainmentAlertParams = { + index: 'testIndex', + indexId: 'testIndexId', + geoField: 'testField', + entity: 'testField', + dateField: 'testField', + boundaryType: 'testType', + boundaryIndexTitle: 'testIndex', + boundaryIndexId: 'testIndexId', + boundaryGeoField: 'testField', + boundaryNameField: '', + }; + expect(validateExpression(initialParams).errors.boundaryGeoField.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts new file mode 100644 index 0000000000000..04fb961ac559b --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts @@ -0,0 +1,86 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; +import { GeoContainmentAlertParams } from './types'; + +export const validateExpression = (alertParams: GeoContainmentAlertParams): ValidationResult => { + const { index, geoField, entity, dateField, boundaryType, boundaryIndexTitle, boundaryGeoField } = + alertParams; + const validationResult = { errors: {} }; + const errors = { + index: new Array(), + indexId: new Array(), + geoField: new Array(), + entity: new Array(), + dateField: new Array(), + boundaryType: new Array(), + boundaryIndexTitle: new Array(), + boundaryIndexId: new Array(), + boundaryGeoField: new Array(), + }; + validationResult.errors = errors; + + if (!index) { + errors.index.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredIndexTitleText', { + defaultMessage: 'Data view is required.', + }) + ); + } + + if (!geoField) { + errors.geoField.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredGeoFieldText', { + defaultMessage: 'Geo field is required.', + }) + ); + } + + if (!entity) { + errors.entity.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredEntityText', { + defaultMessage: 'Entity is required.', + }) + ); + } + + if (!dateField) { + errors.dateField.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredDateFieldText', { + defaultMessage: 'Date field is required.', + }) + ); + } + + if (!boundaryType) { + errors.boundaryType.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredBoundaryTypeText', { + defaultMessage: 'Boundary type is required.', + }) + ); + } + + if (!boundaryIndexTitle) { + errors.boundaryIndexTitle.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredBoundaryIndexTitleText', { + defaultMessage: 'Boundary data view is required.', + }) + ); + } + + if (!boundaryGeoField) { + errors.boundaryGeoField.push( + i18n.translate('xpack.stackAlerts.geoContainment.error.requiredBoundaryGeoFieldText', { + defaultMessage: 'Boundary geo field is required.', + }) + ); + } + + return validationResult; +}; From a55a5ccb6ad3248b71bcedee74792150ef5abe2d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 7 Sep 2023 23:29:19 +0000 Subject: [PATCH 05/15] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/stack_alerts/tsconfig.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 1119b8ca8776b..8eb5d48f39048 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -41,6 +41,12 @@ "@kbn/text-based-languages", "@kbn/text-based-editor", "@kbn/expressions-plugin", + "@kbn/core-http-browser", + "@kbn/core-doc-links-browser", + "@kbn/core-ui-settings-server", + "@kbn/kibana-utils-plugin", + "@kbn/usage-collection-plugin", + "@kbn/react-field", ], "exclude": [ "target/**/*", From 3c73949ed80dd0b1ecebd288285a40f27a236259 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Fri, 8 Sep 2023 14:03:54 -0400 Subject: [PATCH 06/15] fix discover FTR --- .../public/alert_navigation_registry/types.ts | 2 +- x-pack/plugins/alerting/public/plugin.ts | 12 ++++++----- .../public/rule_types/es_query/index.ts | 20 +++++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts index 405f040d5b3ae..136c44b2cd513 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts @@ -16,4 +16,4 @@ import { SanitizedRule } from '../../common'; * originally registered to {@link PluginSetupContract.registerNavigation}. * */ -export type AlertNavigationHandler = (rule: SanitizedRule) => string; +export type AlertNavigationHandler = (rule: SanitizedRule) => string | undefined; diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerting/public/plugin.ts index b4cfc68bc267f..12156734c22d7 100644 --- a/x-pack/plugins/alerting/public/plugin.ts +++ b/x-pack/plugins/alerting/public/plugin.ts @@ -136,14 +136,16 @@ export class AlertingPublicPlugin return; } - if (this.alertNavigationRegistry!.has(rule.consumer, ruleType)) { - const navigationHandler = this.alertNavigationRegistry!.get(rule.consumer, ruleType); - return navigationHandler(rule); - } - if (rule.viewInAppRelativeUrl) { return rule.viewInAppRelativeUrl; } + + if (this.alertNavigationRegistry!.has(rule.consumer, ruleType)) { + const navigationHandler = this.alertNavigationRegistry!.get(rule.consumer, ruleType); + const navUrl = navigationHandler(rule); + if (navUrl) return navUrl; + } + return; }, }; } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts index 6ee6079e6683c..81aeb83c2d75b 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts @@ -10,17 +10,17 @@ import { i18n } from '@kbn/i18n'; import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/public'; import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { ES_QUERY_ID, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { EsQueryRuleParams, SearchType } from './types'; import { validateExpression } from './validation'; +import { isSearchSourceRule } from './util'; const PLUGIN_ID = 'discover'; -const ES_QUERY_ALERT_TYPE = '.es-query'; export function getRuleType(alerting: AlertingSetup): RuleTypeModel { registerNavigation(alerting); - return { - id: ES_QUERY_ALERT_TYPE, + id: ES_QUERY_ID, description: i18n.translate('xpack.stackAlerts.esQuery.ui.alertType.descriptionText', { defaultMessage: 'Alert when matches are found during the latest query run.', }), @@ -46,9 +46,17 @@ export function getRuleType(alerting: AlertingSetup): RuleTypeModel>) => { - return `/app/discover#/viewAlert/${alert.id}`; + ES_QUERY_ID, + (rule: SanitizedRule>) => { + return `/app/discover#/viewAlert/${rule.id}`; + } + ); + alerting.registerNavigation( + STACK_ALERTS_FEATURE_ID, + ES_QUERY_ID, + (rule: SanitizedRule>) => { + if (isSearchSourceRule(rule.params)) return `/app/discover#/viewAlert/${rule.id}`; + return; } ); } From 444bf81e6f77be2617b361ba7442346e2097dd62 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Fri, 8 Sep 2023 14:06:40 -0400 Subject: [PATCH 07/15] move back order --- x-pack/plugins/alerting/public/plugin.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerting/public/plugin.ts index 12156734c22d7..1e8f1721aa126 100644 --- a/x-pack/plugins/alerting/public/plugin.ts +++ b/x-pack/plugins/alerting/public/plugin.ts @@ -136,16 +136,15 @@ export class AlertingPublicPlugin return; } - if (rule.viewInAppRelativeUrl) { - return rule.viewInAppRelativeUrl; - } - if (this.alertNavigationRegistry!.has(rule.consumer, ruleType)) { const navigationHandler = this.alertNavigationRegistry!.get(rule.consumer, ruleType); const navUrl = navigationHandler(rule); if (navUrl) return navUrl; } - return; + + if (rule.viewInAppRelativeUrl) { + return rule.viewInAppRelativeUrl; + } }, }; } From 4c5d5e01f3031bf3c27c434087fc7fa16e1ab8ac Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Fri, 8 Sep 2023 15:27:21 -0400 Subject: [PATCH 08/15] fix old merge issue --- .../security_and_spaces/group2/tests/alerting/alerts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 9a0af4fe756e5..90ddf77f888c3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -1676,7 +1676,7 @@ instanceStateValue: true expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getUnauthorizedErrorMessage( 'create', 'test.always-firing-alert-as-data', 'alertsFixture' From a85cb270953a083e02e5e64abc90f3131441cc33 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Tue, 19 Sep 2023 14:05:59 -0400 Subject: [PATCH 09/15] bad merge --- x-pack/plugins/synthetics/server/feature.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/plugins/synthetics/server/feature.ts b/x-pack/plugins/synthetics/server/feature.ts index f9b6980b6356c..2da2f5a4b93fd 100644 --- a/x-pack/plugins/synthetics/server/feature.ts +++ b/x-pack/plugins/synthetics/server/feature.ts @@ -6,17 +6,12 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -<<<<<<< HEAD -import { ES_QUERY_ID } from '@kbn/rule-data-utils'; -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; -======= -import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; +import { ES_QUERY_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; import { SubFeaturePrivilegeGroupConfig, SubFeaturePrivilegeGroupType, } from '@kbn/features-plugin/common'; ->>>>>>> 3a0ac6fb0e752749a1cbf8f9c4f46468e6948916 import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; From e97fa674b763599f704e539c2324542efe265c5d Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Tue, 19 Sep 2023 14:25:18 -0400 Subject: [PATCH 10/15] fix filterCosumers validation --- .../rule/methods/aggregate/schemas/aggregate_options_schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts index dc2ddb677340a..7250094160a04 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/schemas/aggregate_options_schema.ts @@ -16,7 +16,7 @@ export const aggregateOptionsSchema = schema.object({ id: schema.string(), }) ), - filter_consumers: schema.maybe(schema.arrayOf(schema.string())), + filterConsumers: schema.maybe(schema.arrayOf(schema.string())), // filter type is `string | KueryNode`, but `KueryNode` has no schema to import yet filter: schema.maybe( schema.oneOf([schema.string(), schema.recordOf(schema.string(), schema.any())]) From b47c446612fc34ce8741d6c4245a149ea979be42 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Wed, 20 Sep 2023 20:50:01 -0400 Subject: [PATCH 11/15] only allow log and/or metrics --- .../src/rule_types/o11y_rules.ts | 2 +- .../aggregate/aggregate_rules_route.test.ts | 1 - x-pack/plugins/apm/server/feature.ts | 10 +- .../plugins/observability/common/constants.ts | 3 - .../public/pages/alerts/alerts.tsx | 7 +- x-pack/plugins/observability/server/plugin.ts | 7 +- x-pack/plugins/synthetics/server/feature.ts | 8 +- .../sections/rule_form/rule_add.tsx | 16 ++- .../sections/rule_form/rule_errors.ts | 8 ++ .../sections/rule_form/rule_form.test.tsx | 60 ++++----- .../sections/rule_form/rule_form.tsx | 9 +- .../rule_form_consumer_selection.test.tsx | 82 ++++-------- .../rule_form_consumer_selection.tsx | 123 ++++++++++-------- .../triggers_actions_ui/public/types.ts | 3 - .../check_registered_task_types.ts | 2 +- 15 files changed, 155 insertions(+), 186 deletions(-) diff --git a/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts b/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts index b004d1758d4c7..86c5ef3f2e646 100644 --- a/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts +++ b/packages/kbn-rule-data-utils/src/rule_types/o11y_rules.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.threshold'; +export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.custom_threshold'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts index fc47c19755625..51a7da620dfb3 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/aggregate/aggregate_rules_route.test.ts @@ -237,7 +237,6 @@ describe('aggregateRulesRoute', () => { }, "options": Object { "defaultSearchOperator": "AND", - "filterConsumers": undefined, }, }, ] diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index fe22261fd57f7..79136af2f8856 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -13,21 +13,13 @@ import { LicensingApiRequestHandlerContext, } from '@kbn/licensing-plugin/server'; -import { - ES_QUERY_ID, - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, -} from '@kbn/rule-data-utils'; import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices'; import { ApmRuleType, APM_SERVER_FEATURE_ID, } from '../common/rules/apm_rule_types'; -const ruleTypes = [ - ...Object.values(ApmRuleType), - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - ES_QUERY_ID, -]; +const ruleTypes = Object.values(ApmRuleType); export const APM_FEATURE = { id: APM_SERVER_FEATURE_ID, diff --git a/x-pack/plugins/observability/common/constants.ts b/x-pack/plugins/observability/common/constants.ts index ef3bdea149ba7..11f23808d8fa3 100644 --- a/x-pack/plugins/observability/common/constants.ts +++ b/x-pack/plugins/observability/common/constants.ts @@ -58,9 +58,6 @@ export const observabilityAlertFeatureIds: ValidFeatureId[] = [ ]; export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[] = [ - AlertConsumers.APM, AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS, - AlertConsumers.UPTIME, - AlertConsumers.SLO, ]; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index a95cdce7f9948..27ae07cbf768d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -31,10 +31,7 @@ import { } from '../../components/alert_search_bar/containers'; import { calculateTimeRangeBucketSize } from '../overview/helpers/calculate_bucket_size'; import { getAlertSummaryTimeRange } from '../../utils/alert_summary_widget'; -import { - observabilityAlertFeatureIds, - observabilityRuleCreationValidConsumers, -} from '../../../common/constants'; +import { observabilityAlertFeatureIds } from '../../../common/constants'; import { ALERTS_URL_STORAGE_KEY } from '../../../common/constants'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; @@ -133,7 +130,7 @@ function InternalAlertsPage() { const response = await loadRuleAggregations({ http, typesFilter: filteredRuleTypes, - filterConsumers: observabilityRuleCreationValidConsumers, + filterConsumers: observabilityAlertFeatureIds, }); const { ruleExecutionStatus, ruleMutedStatus, ruleEnabledStatus, ruleSnoozedStatus } = response; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index c3b556757dd52..9d35ced6cb648 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -26,7 +26,6 @@ import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/serve import { SharePluginSetup } from '@kbn/share-plugin/server'; import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { ES_QUERY_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { ObservabilityConfig } from '.'; import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants'; @@ -71,11 +70,7 @@ interface PluginStart { alerting: PluginStartContract; } -const sloRuleTypes = [ - SLO_BURN_RATE_RULE_TYPE_ID, - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - ES_QUERY_ID, -]; +const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID]; export class ObservabilityPlugin implements Plugin { private logger: Logger; diff --git a/x-pack/plugins/synthetics/server/feature.ts b/x-pack/plugins/synthetics/server/feature.ts index 2da2f5a4b93fd..951fdaf33c536 100644 --- a/x-pack/plugins/synthetics/server/feature.ts +++ b/x-pack/plugins/synthetics/server/feature.ts @@ -6,7 +6,6 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { ES_QUERY_ID, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; import { SubFeaturePrivilegeGroupConfig, @@ -26,12 +25,7 @@ const UPTIME_RULE_TYPES = [ 'xpack.uptime.alerts.durationAnomaly', ]; -const ruleTypes = [ - ...UPTIME_RULE_TYPES, - ...SYNTHETICS_RULE_TYPES, - ES_QUERY_ID, - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, -]; +const ruleTypes = [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES]; const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = { groupType: 'independent' as SubFeaturePrivilegeGroupType, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 88b2aa583d031..777639d962307 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -86,7 +86,9 @@ const RuleAdd = ({ props.ruleTypeIndex ); const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); - const [selectedConsumer, setSelectedConsumer] = useState(); + const [selectedConsumer, setSelectedConsumer] = useState< + RuleCreationValidConsumer | null | undefined + >(); const setRule = (value: InitialRule) => { dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); @@ -201,8 +203,16 @@ const RuleAdd = ({ const ruleType = rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null; const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = useMemo( - () => getRuleErrors(rule as Rule, ruleType, config), - [rule, ruleType, config] + () => + getRuleErrors( + { + ...rule, + ...(selectedConsumer !== undefined ? { consumer: selectedConsumer } : {}), + } as Rule, + ruleType, + config + ), + [rule, selectedConsumer, ruleType, config] ); // Confirm before saving if user is able to add actions but hasn't added any to this rule diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts index 02e2e7c084009..0b5c511843472 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts @@ -27,6 +27,7 @@ export function validateBaseProperties( const errors = { name: new Array(), 'schedule.interval': new Array(), + consumer: new Array(), ruleTypeId: new Array(), actionConnectors: new Array(), }; @@ -38,6 +39,13 @@ export function validateBaseProperties( }) ); } + if (ruleObject.consumer === null) { + errors.consumer.push( + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText', { + defaultMessage: 'Scope is required.', + }) + ); + } if (ruleObject.schedule.interval.length < 2) { errors['schedule.interval'].push( i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index c18c011dc6b73..59d3c4f1f8c3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -14,7 +14,7 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { ActionForm } from '../action_connector_form'; -import { AlertConsumers } from '@kbn/rule-data-utils'; +import { AlertConsumers, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { RuleFormConsumerSelection } from './rule_form_consumer_selection'; import { ValidationResult, @@ -261,6 +261,7 @@ describe('rule_form', () => { validConsumers?: RuleCreationValidConsumer[]; ruleTypesOverwrite?: RuleType[]; ruleTypeModelOverwrite?: RuleTypeModel; + useRuleProducer?: boolean; }) { const { showRulesList = false, @@ -271,6 +272,7 @@ describe('rule_form', () => { validConsumers, ruleTypesOverwrite, ruleTypeModelOverwrite, + useRuleProducer = false, } = options || {}; const mocks = coreMock.createSetup(); @@ -343,6 +345,7 @@ describe('rule_form', () => { ruleTypeNonEditable, disabledByLicenseRuleType, ]); + ruleTypeRegistry.get.mockReturnValue(ruleTypeModelOverwrite || ruleType); ruleTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.list.mockReturnValue([actionType]); actionTypeRegistry.has.mockReturnValue(true); @@ -382,6 +385,7 @@ describe('rule_form', () => { onChangeMetaData={jest.fn()} validConsumers={validConsumers} setConsumer={mockSetConsumer} + useRuleProducer={useRuleProducer} /> ); @@ -512,20 +516,20 @@ describe('rule_form', () => { ).toBeTruthy(); }); - it('should display the consumer select for specific rule types', async () => { + it('should select the only one available consumer', async () => { await setup({ initialRuleOverwrite: { name: 'Simple rule', consumer: 'alerts', - ruleTypeId: 'observability.rules.threshold', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, schedule: { interval: '1h', }, }, ruleTypesOverwrite: [ { - id: 'observability.rules.threshold', - name: 'Threshold Rule', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + name: 'Threshold Rule 1', actionGroups: [ { id: 'testActionGroup', @@ -542,11 +546,11 @@ describe('rule_form', () => { apm: { read: true, all: true }, discover: { read: true, all: true }, infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, + // Setting logs all to false, this shouldn't show up + logs: { read: true, all: false }, ml: { read: true, all: true }, monitoring: { read: true, all: true }, siem: { read: true, all: true }, - // Setting SLO all to false, this shouldn't show up slo: { read: true, all: false }, stackAlerts: { read: true, all: true }, uptime: { read: true, all: true }, @@ -559,7 +563,7 @@ describe('rule_form', () => { }, ], ruleTypeModelOverwrite: { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, iconClass: 'test', description: 'test', documentationUrl: null, @@ -569,49 +573,35 @@ describe('rule_form', () => { ruleParamsExpression: TestExpression, requiresAppContext: false, }, - validConsumers: [ - AlertConsumers.APM, - AlertConsumers.INFRASTRUCTURE, - AlertConsumers.LOGS, - AlertConsumers.UPTIME, - AlertConsumers.SLO, - ], + validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS], }); await act(async () => { await nextTick(); wrapper.update(); }); - - expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeTruthy(); - expect(wrapper.find(RuleFormConsumerSelection).props().consumers).toEqual([ - 'apm', - 'infrastructure', - 'logs', - 'uptime', - ]); + expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); await act(async () => { await nextTick(); wrapper.update(); }); - - expect(mockSetConsumer).toHaveBeenLastCalledWith('apm'); + expect(mockSetConsumer).toHaveBeenLastCalledWith('infrastructure'); }); - it('should not display the consumer select if there is only 1 consumer', async () => { + it('should be able to select multiple consumer', async () => { await setup({ initialRuleOverwrite: { name: 'Simple rule', consumer: 'alerts', - ruleTypeId: 'observability.rules.threshold', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, schedule: { interval: '1h', }, }, ruleTypesOverwrite: [ { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', actionGroups: [ { @@ -625,7 +615,8 @@ describe('rule_form', () => { recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, producer: ALERTS_FEATURE_ID, authorizedConsumers: { - apm: { read: true, all: true }, + infrastructure: { read: true, all: true }, + logs: { read: true, all: true }, }, actionVariables: { context: [], @@ -635,7 +626,7 @@ describe('rule_form', () => { }, ], ruleTypeModelOverwrite: { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, iconClass: 'test', description: 'test', documentationUrl: null, @@ -652,14 +643,17 @@ describe('rule_form', () => { wrapper.update(); }); - expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); - + expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeTruthy(); + expect(wrapper.find(RuleFormConsumerSelection).props().consumers).toEqual([ + 'infrastructure', + 'logs', + ]); await act(async () => { await nextTick(); wrapper.update(); }); - expect(mockSetConsumer).toHaveBeenLastCalledWith('apm'); + expect(mockSetConsumer).toHaveBeenLastCalledWith(null); }); it('should not display the consumer select for invalid rule types', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 20d82f1e0f412..88e3639c2dbcf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -134,7 +134,7 @@ interface RuleFormProps> { canShowConsumerSelection?: boolean; setHasActionsDisabled?: (value: boolean) => void; setHasActionsWithBrokenConnector?: (value: boolean) => void; - setConsumer?: (consumer: RuleCreationValidConsumer) => void; + setConsumer?: (consumer: RuleCreationValidConsumer | null) => void; metadata?: MetaData; filteredRuleTypes?: string[]; hideInterval?: boolean; @@ -525,10 +525,7 @@ export const RuleForm = ({ setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); } - if ( - useRuleProducer && - validConsumers?.includes(solution as RuleCreationValidConsumer) - ) { + if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { setConsumer(solution as RuleCreationValidConsumer); } }} @@ -740,8 +737,8 @@ export const RuleForm = ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx index 3ef9a7babe065..ee7af0446c9ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx @@ -10,14 +10,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { RuleFormConsumerSelection } from './rule_form_consumer_selection'; import { RuleCreationValidConsumer } from '../../../types'; -const mockConsumers: RuleCreationValidConsumer[] = [ - 'logs', - 'infrastructure', - 'apm', - 'uptime', - 'slo', - 'stackAlerts', -]; +const mockConsumers: RuleCreationValidConsumer[] = ['logs', 'infrastructure', 'stackAlerts']; const mockOnChange = jest.fn(); @@ -27,81 +20,62 @@ describe('RuleFormConsumerSelectionModal', () => { }); it('renders correctly', async () => { - render(); + render( + + ); expect(screen.getByTestId('ruleFormConsumerSelect')).toBeInTheDocument(); - + fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); expect(screen.getByText('Logs')).toBeInTheDocument(); expect(screen.getByText('Metrics')).toBeInTheDocument(); - expect(screen.getByText('APM and User Experience')).toBeInTheDocument(); - expect(screen.getByText('Synthetics and Uptime')).toBeInTheDocument(); - expect(screen.getByText('SLOs')).toBeInTheDocument(); expect(screen.getByText('Stack Rules')).toBeInTheDocument(); }); - it('should initialize dropdown if provided with a valid initial consumer', () => { - render(); - - // Selects first option if no initial value is provided - expect(mockOnChange).toHaveBeenLastCalledWith('apm'); - mockOnChange.mockClear(); - - // Selects initial consumer + it('should initialize dropdown to null', () => { render( - + ); - expect(mockOnChange).toHaveBeenLastCalledWith('slo'); + // Selects first option if no initial value is provided + expect(mockOnChange).toHaveBeenLastCalledWith(null); mockOnChange.mockClear(); + }); - // Selects first value if provided with invalid consumer + it('should be able to select infrastructure and call onChange', () => { render( - + ); - expect(mockOnChange).toHaveBeenLastCalledWith('apm'); - }); - - it('should select options and call onChange', () => { - render(); - - mockConsumers.forEach((consumer) => { - fireEvent.change(screen.getByTestId('ruleFormConsumerSelect'), { - target: { value: consumer }, - }); - - expect(mockOnChange).toHaveBeenLastCalledWith(consumer); - }); + fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); + fireEvent.click(screen.getByTestId('infrastructure')); + expect(mockOnChange).toHaveBeenLastCalledWith('infrastructure'); }); - it('should default selection to the first option sorted alphabetically', () => { - render(); + it('should be able to select logs and call onChange', () => { + render( + + ); - expect(mockOnChange).toHaveBeenLastCalledWith('apm'); + fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); + fireEvent.click(screen.getByTestId('logs')); + expect(mockOnChange).toHaveBeenLastCalledWith('logs'); }); - it('should default to specified consumer if passed in', () => { + it('should be able to show errors when there is one', () => { render( ); - - expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); + expect(screen.queryAllByText('Scope is required')).toHaveLength(1); }); it('should display nothing if there is only 1 consumer to select', () => { - render(); + render( + + ); expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx index b2e08493fdcff..f5f92c72fa521 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx @@ -6,10 +6,10 @@ */ import React, { useMemo, useState, useCallback, useEffect } from 'react'; -import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { RuleCreationValidConsumer } from '../../../types'; +import { IErrorObject, RuleCreationValidConsumer } from '../../../types'; const SELECT_LABEL: string = i18n.translate( 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel', @@ -60,82 +60,97 @@ const featureNameMap: Record = { export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ AlertConsumers.LOGS, AlertConsumers.INFRASTRUCTURE, - AlertConsumers.APM, - AlertConsumers.UPTIME, - AlertConsumers.SLO, 'stackAlerts', ]; export interface RuleFormConsumerSelectionProps { consumers: RuleCreationValidConsumer[]; - initialConsumer?: RuleCreationValidConsumer; - onChange: (consumer: RuleCreationValidConsumer) => void; + onChange: (consumer: RuleCreationValidConsumer | null) => void; + errors: IErrorObject; } -export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps) => { - const { consumers, initialConsumer, onChange } = props; - const [selectedConsumer, setSelectedConsumer] = useState(); +const SINGLE_SELECTION = { asPlainText: true }; +export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps) => { + const { consumers, errors, onChange } = props; + const [selectedConsumer, setSelectedConsumer] = useState(); + const isInvalid = errors?.consumer?.length > 0; const handleOnChange = useCallback( - (e: React.ChangeEvent) => { - setSelectedConsumer(e.target.value as RuleCreationValidConsumer); - onChange(e.target.value as RuleCreationValidConsumer); + (selected: Array>) => { + if (selected.length > 0) { + const newSelectedConsumer = selected[0]; + setSelectedConsumer(newSelectedConsumer.value); + onChange(newSelectedConsumer.value!); + } else { + setSelectedConsumer(undefined); + onChange(null); + } }, - [setSelectedConsumer, onChange] + [onChange] + ); + const selectedOptions = useMemo( + () => + selectedConsumer + ? [{ value: selectedConsumer, label: featureNameMap[selectedConsumer] }] + : [], + [selectedConsumer] ); - const formattedSelectOptions: EuiSelectOption[] = useMemo(() => { - return consumers - .reduce((result, consumer) => { - if (featureNameMap[consumer]) { - result.push({ - value: consumer, - text: featureNameMap[consumer], - }); - } - return result; - }, []) - .sort((a, b) => { - return (a.value as RuleCreationValidConsumer).localeCompare( - b.value as RuleCreationValidConsumer - ); - }); - }, [consumers]); + const formattedSelectOptions: Array> = + useMemo(() => { + return consumers + .reduce>>((result, consumer) => { + if (featureNameMap[consumer]) { + result.push({ + value: consumer, + 'data-test-subj': consumer, + label: featureNameMap[consumer], + }); + } + return result; + }, []) + .sort((a, b) => { + return a.value!.localeCompare(b.value!); + }); + }, [consumers]); - // Initialize dropdown with the initial consumer, otherwise the first option useEffect(() => { - if (selectedConsumer || formattedSelectOptions.length === 0) { - return; - } + // At initialization we set to NULL to know that nobody selected anything + onChange(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - if ( - initialConsumer && - formattedSelectOptions.find((option) => option.value === initialConsumer) - ) { - setSelectedConsumer(initialConsumer); - onChange(initialConsumer); - return; + useEffect(() => { + if (formattedSelectOptions.length === 1) { + onChange(formattedSelectOptions[0].value as RuleCreationValidConsumer); } - - const firstConsumer = formattedSelectOptions[0].value as RuleCreationValidConsumer; - setSelectedConsumer(firstConsumer); - onChange(firstConsumer); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedConsumer, formattedSelectOptions, initialConsumer]); + }, [formattedSelectOptions]); if (formattedSelectOptions.length <= 1) { return null; } - return ( - - + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 48f99f765df49..7716aef340406 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -826,7 +826,4 @@ export interface NotifyWhenSelectOptions { export type RuleCreationValidConsumer = | typeof AlertConsumers.LOGS | typeof AlertConsumers.INFRASTRUCTURE - | typeof AlertConsumers.APM - | typeof AlertConsumers.UPTIME - | typeof AlertConsumers.SLO | typeof STACK_ALERTS_FEATURE_ID; diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index b91e34e292981..a5f039eaed5f2 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -97,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) { 'alerting:monitoring_alert_thread_pool_write_rejections', 'alerting:monitoring_ccr_read_exceptions', 'alerting:monitoring_shard_size', - 'alerting:observability.rules.threshold', + 'alerting:observability.rules.custom_threshold', 'alerting:siem.eqlRule', 'alerting:siem.indicatorRule', 'alerting:siem.mlRule', From caa31806c6cd9d2fb744b2addbc7558a8f6efa0a Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Wed, 20 Sep 2023 21:25:18 -0400 Subject: [PATCH 12/15] fix rule add test --- .../sections/rule_form/rule_add.test.tsx | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index 4861fb77393c0..4409be029092c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -16,7 +16,7 @@ import RuleAdd from './rule_add'; import { createRule } from '../../lib/rule_api/create'; import { alertingFrameworkHealth } from '../../lib/rule_api/health'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { AlertConsumers } from '@kbn/rule-data-utils'; +import { AlertConsumers, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { Rule, RuleAddProps, @@ -340,7 +340,7 @@ describe('rule_add', () => { }); }); - it('should set consumer when the consumer selection renders and rule is created', async () => { + it('should NOT allow to save the rule if the consumer is not set', async () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false }, }); @@ -349,7 +349,7 @@ describe('rule_add', () => { initialValues: { name: 'Simple rule', consumer: 'alerts', - ruleTypeId: 'observability.rules.threshold', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, tags: ['uptime', 'logs'], schedule: { interval: '1h', @@ -358,7 +358,7 @@ describe('rule_add', () => { onClose, ruleTypesOverwrite: [ { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', actionGroups: [ { @@ -393,7 +393,7 @@ describe('rule_add', () => { }, ], ruleTypeModelOverwrite: { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, iconClass: 'test', description: 'test', documentationUrl: null, @@ -403,13 +403,7 @@ describe('rule_add', () => { ruleParamsExpression: TestExpression, requiresAppContext: false, }, - validConsumers: [ - AlertConsumers.APM, - AlertConsumers.INFRASTRUCTURE, - AlertConsumers.LOGS, - AlertConsumers.UPTIME, - AlertConsumers.SLO, - ], + validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS], }); await act(async () => { @@ -424,13 +418,7 @@ describe('rule_add', () => { wrapper.update(); }); - expect(createRule).toHaveBeenLastCalledWith( - expect.objectContaining({ - rule: expect.objectContaining({ - consumer: 'apm', - }), - }) - ); + expect(createRule).toBeCalledTimes(0); }); it('should set consumer automatically if only 1 authorized consumer exists', async () => { @@ -442,7 +430,7 @@ describe('rule_add', () => { initialValues: { name: 'Simple rule', consumer: 'alerts', - ruleTypeId: 'observability.rules.threshold', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, tags: ['uptime', 'logs'], schedule: { interval: '1h', @@ -451,7 +439,7 @@ describe('rule_add', () => { onClose, ruleTypesOverwrite: [ { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', actionGroups: [ { @@ -475,7 +463,7 @@ describe('rule_add', () => { }, ], ruleTypeModelOverwrite: { - id: 'observability.rules.threshold', + id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, iconClass: 'test', description: 'test', documentationUrl: null, @@ -485,13 +473,7 @@ describe('rule_add', () => { ruleParamsExpression: TestExpression, requiresAppContext: false, }, - validConsumers: [ - AlertConsumers.APM, - AlertConsumers.INFRASTRUCTURE, - AlertConsumers.LOGS, - AlertConsumers.UPTIME, - AlertConsumers.SLO, - ], + validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS], }); await act(async () => { From 79eed87c69fb33cd498df22672f2b72097b84b20 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Thu, 21 Sep 2023 13:05:48 -0400 Subject: [PATCH 13/15] fix tests --- .../sections/rule_form/rule_errors.test.tsx | 34 +++++++++++++++++++ .../apps/observability/pages/rules_page.ts | 16 +++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx index 0e4bd4108c271..ad4f081e3d625 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx @@ -29,6 +29,7 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: [], actionConnectors: [], + consumer: [], }); }); @@ -41,6 +42,7 @@ describe('rule_errors', () => { 'schedule.interval': ['Check interval is required.'], ruleTypeId: [], actionConnectors: [], + consumer: [], }); }); @@ -53,6 +55,7 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: [], actionConnectors: [], + consumer: [], }); }); @@ -68,6 +71,7 @@ describe('rule_errors', () => { 'schedule.interval': ['Interval must be at least 1 minute.'], ruleTypeId: [], actionConnectors: [], + consumer: [], }); }); @@ -80,6 +84,33 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: ['Rule type is required.'], actionConnectors: [], + consumer: [], + }); + }); + + it('should get an error when consumer is null', () => { + const rule = mockRule(); + rule.consumer = null as unknown as string; + const result = validateBaseProperties(rule, config); + expect(result.errors).toStrictEqual({ + name: [], + 'schedule.interval': [], + ruleTypeId: [], + actionConnectors: [], + consumer: ['Scope is required.'], + }); + }); + + it('should not get an error when consumer is undefined', () => { + const rule = mockRule(); + rule.consumer = undefined as unknown as string; + const result = validateBaseProperties(rule, config); + expect(result.errors).toStrictEqual({ + name: [], + 'schedule.interval': [], + ruleTypeId: [], + actionConnectors: [], + consumer: [], }); }); @@ -101,6 +132,7 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: [], actionConnectors: ['Action for myActionType connector is required.'], + consumer: [], }); }); }); @@ -127,6 +159,7 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: [], actionConnectors: [], + consumer: [], }, ruleErrors: { name: ['Name is required.'], @@ -134,6 +167,7 @@ describe('rule_errors', () => { 'schedule.interval': [], ruleTypeId: [], actionConnectors: [], + consumer: [], }, }); }); diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index 1c7f17961db81..e912a52d36a18 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -185,12 +185,16 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); const consumerSelect = await testSubjects.find('ruleFormConsumerSelect'); - const consumerOptions = await consumerSelect.findAllByTagName('option'); - - // There seems to be an extra option, so assert options + 1 - expect(consumerOptions.length).eql(3); - expect(await consumerOptions[1].getAttribute('value')).eql('infrastructure'); - expect(await consumerOptions[2].getAttribute('value')).eql('logs'); + await consumerSelect.click(); + const consumerOptionsList = await testSubjects.find( + 'comboBoxOptionsList ruleFormConsumerSelect-optionsList' + ); + const consumerOptions = await consumerOptionsList.findAllByClassName( + 'euiComboBoxOption__content' + ); + expect(consumerOptions.length).eql(2); + expect(await consumerOptions[0].getVisibleText()).eql('Metrics'); + expect(await consumerOptions[1].getVisibleText()).eql('Logs'); }); }); From a6babf2922a3a87033c5cbec585abe500855f2f2 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Thu, 21 Sep 2023 14:31:14 -0400 Subject: [PATCH 14/15] fix view rule list in o11y --- x-pack/plugins/observability/public/pages/rules/rules_tab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx index 22b321262943c..a2939bb4876e9 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules_tab.tsx @@ -11,7 +11,7 @@ import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { RuleStatus } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../utils/kibana_react'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; -import { observabilityRuleCreationValidConsumers } from '../../../common/constants'; +import { observabilityAlertFeatureIds } from '../../../common/constants'; interface RulesTabProps { setRefresh: React.Dispatch>; @@ -74,7 +74,7 @@ export function RulesTab({ setRefresh, stateRefresh }: RulesTabProps) { return ( Date: Thu, 21 Sep 2023 14:47:54 -0400 Subject: [PATCH 15/15] fix esQuery registration view in app --- .../public/rule_types/es_query/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts index 81aeb83c2d75b..7ffd10adce528 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/index.ts @@ -59,4 +59,20 @@ function registerNavigation(alerting: AlertingSetup) { return; } ); + alerting.registerNavigation( + 'logs', + ES_QUERY_ID, + (rule: SanitizedRule>) => { + if (isSearchSourceRule(rule.params)) return `/app/discover#/viewAlert/${rule.id}`; + return; + } + ); + alerting.registerNavigation( + 'infrastructure', + ES_QUERY_ID, + (rule: SanitizedRule>) => { + if (isSearchSourceRule(rule.params)) return `/app/discover#/viewAlert/${rule.id}`; + return; + } + ); }