From 93614af6a9134e5f3a22b139fae445e9f44b9ffa Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 9 Dec 2020 15:09:58 -0800 Subject: [PATCH] [Alerts][License] Add license checks to alerts HTTP APIs and execution (#85223) * [Alerts][License] Add license checks to alerts HTTP APIs and execution * fixed typechecks * resolved conflicts * resolved conflicts * added router tests * fixed typechecks * added license check support for alert task running * fixed typechecks * added integration tests * fixed due to comments * fixed due to comments * fixed tests * fixed typechecks --- .../actions/server/action_type_registry.ts | 2 +- x-pack/plugins/alerts/common/alert.ts | 1 + .../alerts/server/alert_type_registry.mock.ts | 1 + .../alerts/server/alert_type_registry.test.ts | 60 ++++- .../alerts/server/alert_type_registry.ts | 31 ++- .../server/alerts_client/alerts_client.ts | 18 ++ .../alerts_client/tests/aggregate.test.ts | 5 +- .../server/alerts_client/tests/create.test.ts | 10 + .../server/alerts_client/tests/find.test.ts | 5 +- .../tests/list_alert_types.test.ts | 21 +- .../alerts_authorization.test.ts | 24 +- .../alerts_authorization_kuery.test.ts | 5 + .../server/lib/errors/alert_type_disabled.ts | 27 ++ .../plugins/alerts/server/lib/errors/types.ts | 11 + .../lib/get_alert_type_feature_usage_name.ts | 9 + .../alerts/server/lib/license_api_access.ts | 4 +- .../alerts/server/lib/license_state.mock.ts | 45 ++-- .../alerts/server/lib/license_state.test.ts | 239 +++++++++++++++++- .../alerts/server/lib/license_state.ts | 100 +++++++- x-pack/plugins/alerts/server/plugin.test.ts | 2 + x-pack/plugins/alerts/server/plugin.ts | 13 +- .../alerts/server/routes/aggregate.test.ts | 8 +- .../plugins/alerts/server/routes/aggregate.ts | 4 +- .../alerts/server/routes/create.test.ts | 26 +- x-pack/plugins/alerts/server/routes/create.ts | 20 +- .../alerts/server/routes/delete.test.ts | 8 +- x-pack/plugins/alerts/server/routes/delete.ts | 4 +- .../alerts/server/routes/disable.test.ts | 25 +- .../plugins/alerts/server/routes/disable.ts | 16 +- .../alerts/server/routes/enable.test.ts | 25 +- x-pack/plugins/alerts/server/routes/enable.ts | 16 +- .../plugins/alerts/server/routes/find.test.ts | 8 +- x-pack/plugins/alerts/server/routes/find.ts | 4 +- .../plugins/alerts/server/routes/get.test.ts | 8 +- x-pack/plugins/alerts/server/routes/get.ts | 4 +- .../routes/get_alert_instance_summary.test.ts | 6 +- .../routes/get_alert_instance_summary.ts | 4 +- .../server/routes/get_alert_state.test.ts | 8 +- .../alerts/server/routes/get_alert_state.ts | 4 +- .../alerts/server/routes/health.test.ts | 18 +- x-pack/plugins/alerts/server/routes/health.ts | 4 +- .../server/routes/list_alert_types.test.ts | 19 +- .../alerts/server/routes/list_alert_types.ts | 4 +- .../alerts/server/routes/mute_all.test.ts | 25 +- .../plugins/alerts/server/routes/mute_all.ts | 16 +- .../server/routes/mute_instance.test.ts | 27 +- .../alerts/server/routes/mute_instance.ts | 16 +- .../alerts/server/routes/unmute_all.test.ts | 25 +- .../alerts/server/routes/unmute_all.ts | 16 +- .../server/routes/unmute_instance.test.ts | 27 +- .../alerts/server/routes/unmute_instance.ts | 16 +- .../alerts/server/routes/update.test.ts | 29 ++- x-pack/plugins/alerts/server/routes/update.ts | 21 +- .../server/routes/update_api_key.test.ts | 27 +- .../alerts/server/routes/update_api_key.ts | 16 +- .../server/task_runner/task_runner.test.ts | 74 +++++- .../alerts/server/task_runner/task_runner.ts | 8 + .../task_runner/task_runner_factory.test.ts | 2 + .../server/task_runner/task_runner_factory.ts | 3 +- .../sections/alerts_list/translations.ts | 8 + .../tests/alerts/basic_noop_alert_type.ts | 23 ++ .../tests/alerts/gold_noop_alert_type.ts | 28 ++ .../basic/tests/alerts/index.ts | 15 ++ .../basic/tests/index.ts | 1 + .../plugins/alerts/server/alert_types.ts | 10 + .../tests/alerting/list_alert_types.ts | 2 + .../tests/alerting/list_alert_types.ts | 1 + 67 files changed, 1129 insertions(+), 183 deletions(-) create mode 100644 x-pack/plugins/alerts/server/lib/errors/alert_type_disabled.ts create mode 100644 x-pack/plugins/alerts/server/lib/errors/types.ts create mode 100644 x-pack/plugins/alerts/server/lib/get_alert_type_feature_usage_name.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/alerts/index.ts diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 15ff7e558025e..7176d3ad3a1a7 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -177,7 +177,7 @@ export class ActionTypeRegistry { minimumLicenseRequired: actionType.minimumLicenseRequired, enabled: this.isActionTypeEnabled(actionTypeId), enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), - enabledInLicense: this.licenseState.isLicenseValidForActionType(actionType).isValid === true, + enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid, })); } } diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index 88f6090d20737..2f81d74ac8b3c 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -25,6 +25,7 @@ export enum AlertExecutionStatusErrorReasons { Decrypt = 'decrypt', Execute = 'execute', Unknown = 'unknown', + License = 'license', } export interface AlertExecutionStatus { diff --git a/x-pack/plugins/alerts/server/alert_type_registry.mock.ts b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts index 39d15eba014c9..f41023c189229 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.mock.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts @@ -14,6 +14,7 @@ const createAlertTypeRegistryMock = () => { register: jest.fn(), get: jest.fn(), list: jest.fn(), + ensureAlertTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index ef481788d836f..58b2cb74f2353 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -5,17 +5,27 @@ */ import { TaskRunnerFactory } from './task_runner'; -import { AlertTypeRegistry } from './alert_type_registry'; +import { AlertTypeRegistry, ConstructorOptions } from './alert_type_registry'; import { AlertType } from './types'; import { taskManagerMock } from '../../task_manager/server/mocks'; +import { ILicenseState } from './lib/license_state'; +import { licenseStateMock } from './lib/license_state.mock'; +import { licensingMock } from '../../licensing/server/mocks'; +let mockedLicenseState: jest.Mocked; +let alertTypeRegistryParams: ConstructorOptions; const taskManager = taskManagerMock.createSetup(); -const alertTypeRegistryParams = { - taskManager, - taskRunnerFactory: new TaskRunnerFactory(), -}; -beforeEach(() => jest.resetAllMocks()); +beforeEach(() => { + jest.resetAllMocks(); + mockedLicenseState = licenseStateMock.create(); + alertTypeRegistryParams = { + taskManager, + taskRunnerFactory: new TaskRunnerFactory(), + licenseState: mockedLicenseState, + licensing: licensingMock.createSetup(), + }; +}); describe('has()', () => { test('returns false for unregistered alert types', () => { @@ -379,6 +389,7 @@ describe('list()', () => { "state": Array [], }, "defaultActionGroupId": "testActionGroup", + "enabledInLicense": false, "id": "test", "minimumLicenseRequired": "basic", "name": "Test", @@ -427,6 +438,43 @@ describe('list()', () => { }); }); +describe('ensureAlertTypeEnabled', () => { + let alertTypeRegistry: AlertTypeRegistry; + + beforeEach(() => { + alertTypeRegistry = new AlertTypeRegistry(alertTypeRegistryParams); + alertTypeRegistry.register({ + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + producer: 'alerts', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + }); + }); + + test('should call ensureLicenseForAlertType on the license state', async () => { + alertTypeRegistry.ensureAlertTypeEnabled('test'); + expect(mockedLicenseState.ensureLicenseForAlertType).toHaveBeenCalled(); + }); + + test('should throw when ensureLicenseForAlertType throws', async () => { + mockedLicenseState.ensureLicenseForAlertType.mockImplementation(() => { + throw new Error('Fail'); + }); + expect(() => + alertTypeRegistry.ensureAlertTypeEnabled('test') + ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); +}); + function alertTypeWithVariables(id: string, context: string, state: string): AlertType { const baseAlert: AlertType = { id, diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index e7571a71eb321..d436d1987c027 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import typeDetect from 'type-detect'; import { intersection } from 'lodash'; +import { LicensingPluginSetup } from '../../licensing/server'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { TaskRunnerFactory } from './task_runner'; import { @@ -19,10 +20,14 @@ import { AlertInstanceContext, } from './types'; import { RecoveredActionGroup, getBuiltinActionGroups } from '../common'; +import { ILicenseState } from './lib/license_state'; +import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name'; -interface ConstructorOptions { +export interface ConstructorOptions { taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; + licenseState: ILicenseState; + licensing: LicensingPluginSetup; } export interface RegistryAlertType @@ -34,8 +39,10 @@ export interface RegistryAlertType | 'defaultActionGroupId' | 'actionVariables' | 'producer' + | 'minimumLicenseRequired' > { id: string; + enabledInLicense: boolean; } /** @@ -70,16 +77,24 @@ export class AlertTypeRegistry { private readonly taskManager: TaskManagerSetupContract; private readonly alertTypes: Map = new Map(); private readonly taskRunnerFactory: TaskRunnerFactory; + private readonly licenseState: ILicenseState; + private readonly licensing: LicensingPluginSetup; - constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) { + constructor({ taskManager, taskRunnerFactory, licenseState, licensing }: ConstructorOptions) { this.taskManager = taskManager; this.taskRunnerFactory = taskRunnerFactory; + this.licenseState = licenseState; + this.licensing = licensing; } public has(id: string) { return this.alertTypes.has(id); } + public ensureAlertTypeEnabled(id: string) { + this.licenseState.ensureLicenseForAlertType(this.get(id)); + } + public register< Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, @@ -108,6 +123,13 @@ export class AlertTypeRegistry { this.taskRunnerFactory.create(normalizedAlertType, context), }, }); + // No need to notify usage on basic alert types + if (alertType.minimumLicenseRequired !== 'basic') { + this.licensing.featureUsage.register( + getAlertTypeFeatureUsageName(alertType.name), + alertType.minimumLicenseRequired + ); + } } public get< @@ -157,6 +179,11 @@ export class AlertTypeRegistry { actionVariables, producer, minimumLicenseRequired, + enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType( + id, + name, + minimumLicenseRequired + ).isValid, }) ) ); diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index d697817be734b..b936ac53d63c7 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -236,6 +236,8 @@ export class AlertsClient { throw error; } + this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId); + // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); @@ -644,6 +646,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); + const updateResult = await this.updateAlert({ id, data }, alertSavedObject); await Promise.all([ @@ -819,6 +823,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { @@ -902,6 +908,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + if (attributes.enabled === false) { const username = await this.getUserName(); const updateAttributes = this.updateMeta({ @@ -1001,6 +1009,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', @@ -1075,6 +1085,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const updateAttributes = this.updateMeta({ muteAll: true, mutedInstanceIds: [], @@ -1134,6 +1146,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const updateAttributes = this.updateMeta({ muteAll: false, mutedInstanceIds: [], @@ -1193,6 +1207,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); @@ -1257,6 +1273,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { await this.unsecuredSavedObjectsClient.update( diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts index bf796466932d4..81b095c013e71 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts @@ -15,6 +15,7 @@ import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup, setGlobalDate } from './lib'; import { AlertExecutionStatusValues } from '../../types'; import { RecoveredActionGroup } from '../../../common'; +import { RegistryAlertType } from '../../alert_type_registry'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -49,7 +50,7 @@ beforeEach(() => { setGlobalDate(); describe('aggregate()', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], actionVariables: undefined, @@ -59,6 +60,7 @@ describe('aggregate()', () => { id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -111,6 +113,7 @@ describe('aggregate()', () => { authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index cada7ad7a7132..bfbc3daafd90b 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -1194,4 +1194,14 @@ describe('create()', () => { } ); }); + + test('throws error when ensureActionTypeEnabled throws', async () => { + const data = getMockData(); + alertTypeRegistry.ensureAlertTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail"` + ); + }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index 9741eca1c3c9c..ebb9b1aaf4149 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -18,6 +18,7 @@ import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; +import { RegistryAlertType } from '../../alert_type_registry'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -53,7 +54,7 @@ beforeEach(() => { setGlobalDate(); describe('find()', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], recoveryActionGroup: RecoveredActionGroup, @@ -63,6 +64,7 @@ describe('find()', () => { id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -121,6 +123,7 @@ describe('find()', () => { authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts index 3f7bf57cfc789..ddb4778821905 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -10,10 +10,14 @@ import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { + AlertsAuthorization, + RegistryAlertTypeWithAuth, +} from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; import { RecoveredActionGroup } from '../../../common'; +import { RegistryAlertType } from '../../alert_type_registry'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -47,7 +51,7 @@ beforeEach(() => { describe('listAlertTypes', () => { let alertsClient: AlertsClient; - const alertingAlertType = { + const alertingAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -56,8 +60,9 @@ describe('listAlertTypes', () => { id: 'alertingAlertType', name: 'alertingAlertType', producer: 'alerts', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -66,6 +71,7 @@ describe('listAlertTypes', () => { id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); @@ -82,7 +88,7 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); authorization.filterByAlertTypeAuthorization.mockResolvedValue( - new Set([ + new Set([ { ...myAppAlertType, authorizedConsumers }, { ...alertingAlertType, authorizedConsumers }, ]) @@ -96,7 +102,7 @@ describe('listAlertTypes', () => { }); describe('authorization', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], actionVariables: undefined, @@ -106,6 +112,7 @@ describe('listAlertTypes', () => { id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, { id: 'myOtherType', @@ -115,6 +122,7 @@ describe('listAlertTypes', () => { minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -122,7 +130,7 @@ describe('listAlertTypes', () => { }); test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { - const authorizedTypes = new Set([ + const authorizedTypes = new Set([ { id: 'myType', name: 'Test', @@ -134,6 +142,7 @@ describe('listAlertTypes', () => { authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]); authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 1914bbcb0acba..a7d9421073483 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -17,6 +17,7 @@ import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import uuid from 'uuid'; import { RecoveredActionGroup } from '../../common'; +import { RegistryAlertType } from '../alert_type_registry'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); @@ -533,7 +534,7 @@ describe('AlertsAuthorization', () => { }); describe('getFindAuthorizationFilter', () => { - const myOtherAppAlertType = { + const myOtherAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -542,8 +543,9 @@ describe('AlertsAuthorization', () => { id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', producer: 'alerts', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -552,8 +554,9 @@ describe('AlertsAuthorization', () => { id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; - const mySecondAppAlertType = { + const mySecondAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -562,6 +565,7 @@ describe('AlertsAuthorization', () => { id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); @@ -829,7 +833,7 @@ describe('AlertsAuthorization', () => { }); describe('filterByAlertTypeAuthorization', () => { - const myOtherAppAlertType = { + const myOtherAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -838,8 +842,9 @@ describe('AlertsAuthorization', () => { id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', producer: 'myOtherApp', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', @@ -848,6 +853,7 @@ describe('AlertsAuthorization', () => { id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); @@ -890,6 +896,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", "minimumLicenseRequired": "basic", "name": "myAppAlertType", @@ -921,6 +928,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", @@ -992,6 +1000,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", @@ -1019,6 +1028,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", "minimumLicenseRequired": "basic", "name": "myAppAlertType", @@ -1085,6 +1095,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", "minimumLicenseRequired": "basic", "name": "myAppAlertType", @@ -1180,6 +1191,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", @@ -1207,6 +1219,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", "minimumLicenseRequired": "basic", "name": "myAppAlertType", @@ -1286,6 +1299,7 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts index 976a9f40f8d66..8249047c0ef39 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization_kuery.test.ts @@ -26,6 +26,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ) @@ -53,6 +54,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myApp: { read: true, all: true }, myOtherApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ) @@ -81,6 +83,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myOtherApp: { read: true, all: true }, myAppWithSubFeature: { read: true, all: true }, }, + enabledInLicense: true, }, { actionGroups: [], @@ -96,6 +99,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myOtherApp: { read: true, all: true }, myAppWithSubFeature: { read: true, all: true }, }, + enabledInLicense: true, }, { actionGroups: [], @@ -111,6 +115,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myOtherApp: { read: true, all: true }, myAppWithSubFeature: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ) diff --git a/x-pack/plugins/alerts/server/lib/errors/alert_type_disabled.ts b/x-pack/plugins/alerts/server/lib/errors/alert_type_disabled.ts new file mode 100644 index 0000000000000..9a8ebc61118c3 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/errors/alert_type_disabled.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaResponseFactory } from '../../../../../../src/core/server'; +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export type AlertTypeDisabledReason = + | 'config' + | 'license_unavailable' + | 'license_invalid' + | 'license_expired'; + +export class AlertTypeDisabledError extends Error implements ErrorThatHandlesItsOwnResponse { + public readonly reason: AlertTypeDisabledReason; + + constructor(message: string, reason: AlertTypeDisabledReason) { + super(message); + this.reason = reason; + } + + public sendResponse(res: KibanaResponseFactory) { + return res.forbidden({ body: { message: this.message } }); + } +} diff --git a/x-pack/plugins/alerts/server/lib/errors/types.ts b/x-pack/plugins/alerts/server/lib/errors/types.ts new file mode 100644 index 0000000000000..949dc348265ae --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/errors/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaResponseFactory, IKibanaResponse } from '../../../../../../src/core/server'; + +export interface ErrorThatHandlesItsOwnResponse extends Error { + sendResponse(res: KibanaResponseFactory): IKibanaResponse; +} diff --git a/x-pack/plugins/alerts/server/lib/get_alert_type_feature_usage_name.ts b/x-pack/plugins/alerts/server/lib/get_alert_type_feature_usage_name.ts new file mode 100644 index 0000000000000..cd7c89d391e9b --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/get_alert_type_feature_usage_name.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getAlertTypeFeatureUsageName(alertTypeName: string) { + return `Alert: ${alertTypeName}`; +} diff --git a/x-pack/plugins/alerts/server/lib/license_api_access.ts b/x-pack/plugins/alerts/server/lib/license_api_access.ts index f9ef51f6b3c9a..ddbcaa90dcee1 100644 --- a/x-pack/plugins/alerts/server/lib/license_api_access.ts +++ b/x-pack/plugins/alerts/server/lib/license_api_access.ts @@ -5,9 +5,9 @@ */ import Boom from '@hapi/boom'; -import { LicenseState } from './license_state'; +import { ILicenseState } from './license_state'; -export function verifyApiAccess(licenseState: LicenseState) { +export function verifyApiAccess(licenseState: ILicenseState) { const licenseCheckResults = licenseState.getLicenseInformation(); if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { diff --git a/x-pack/plugins/alerts/server/lib/license_state.mock.ts b/x-pack/plugins/alerts/server/lib/license_state.mock.ts index aaccbfcc0af0e..0bab8e65af168 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.mock.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.mock.ts @@ -4,35 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { of } from 'rxjs'; -import { LicenseState } from './license_state'; -import { ILicense } from '../../../licensing/server'; +import { ILicenseState } from './license_state'; -export const mockLicenseState = () => { - const license: ILicense = { - uid: '123', - status: 'active', - isActive: true, - signature: 'sig', - isAvailable: true, - toJSON: () => ({ - signature: 'sig', +export const createLicenseStateMock = () => { + const licenseState: jest.Mocked = { + clean: jest.fn(), + getLicenseInformation: jest.fn(), + ensureLicenseForAlertType: jest.fn(), + getLicenseCheckForAlertType: jest.fn().mockResolvedValue({ + isValid: true, }), - getUnavailableReason: () => undefined, - hasAtLeast() { - return true; - }, - check() { - return { - state: 'valid', - }; - }, - getFeature() { - return { - isAvailable: true, - isEnabled: true, - }; - }, + checkLicense: jest.fn().mockResolvedValue({ + state: 'valid', + }), + setNotifyUsage: jest.fn(), }; - return new LicenseState(of(license)); + return licenseState; +}; + +export const licenseStateMock = { + create: createLicenseStateMock, }; diff --git a/x-pack/plugins/alerts/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts index 50b4e6b4439f7..7694287b1f83e 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { LicenseState } from './license_state'; +import { AlertType } from '../types'; +import { Subject } from 'rxjs'; +import { LicenseState, ILicenseState } from './license_state'; import { licensingMock } from '../../../licensing/server/mocks'; +import { ILicense } from '../../../licensing/server'; -describe('license_state', () => { +describe('checkLicense()', () => { const getRawLicense = jest.fn(); beforeEach(() => { @@ -27,8 +29,8 @@ describe('license_state', () => { it('check application link should be disabled', () => { const licensing = licensingMock.createSetup(); const licenseState = new LicenseState(licensing.license$); - const alertingLicenseInfo = licenseState.checkLicense(getRawLicense()); - expect(alertingLicenseInfo.enableAppLink).to.be(false); + const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(actionsLicenseInfo.enableAppLink).toBe(false); }); }); @@ -44,8 +46,231 @@ describe('license_state', () => { it('check application link should be enabled', () => { const licensing = licensingMock.createSetup(); const licenseState = new LicenseState(licensing.license$); - const alertingLicenseInfo = licenseState.checkLicense(getRawLicense()); - expect(alertingLicenseInfo.enableAppLink).to.be(true); + const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(actionsLicenseInfo.showAppLink).toBe(true); }); }); }); + +describe('getLicenseCheckForAlertType', () => { + let license: Subject; + let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + producer: 'alerts', + minimumLicenseRequired: 'gold', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + }; + + beforeEach(() => { + license = new Subject(); + licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); + }); + + test('should return false when license not defined', () => { + expect( + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ) + ).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license not available', () => { + license.next(createUnavailableLicense()); + expect( + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ) + ).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license is expired', () => { + const expiredLicense = licensingMock.createLicense({ license: { status: 'expired' } }); + license.next(expiredLicense); + expect( + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ) + ).toEqual({ + isValid: false, + reason: 'expired', + }); + }); + + test('should return false when license is invalid', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + expect( + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ) + ).toEqual({ + isValid: false, + reason: 'invalid', + }); + }); + + test('should return true when license is valid', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + expect( + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ) + ).toEqual({ + isValid: true, + }); + }); + + test('should not call notifyUsage by default', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.getLicenseCheckForAlertType(alertType.id, alertType.name, 'gold'); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should not call notifyUsage on basic action types', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + licenseState.getLicenseCheckForAlertType(alertType.id, alertType.name, 'basic'); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should call notifyUsage when specified', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired, + { notifyUsage: true } + ); + expect(mockNotifyUsage).toHaveBeenCalledWith('Alert: Test'); + }); +}); + +describe('ensureLicenseForAlertType()', () => { + let license: Subject; + let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + producer: 'alerts', + minimumLicenseRequired: 'gold', + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + }; + + beforeEach(() => { + license = new Subject(); + licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); + }); + + test('should throw when license not defined', () => { + expect(() => + licenseState.ensureLicenseForAlertType(alertType) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert type test is disabled because license information is not available at this time."` + ); + }); + + test('should throw when license not available', () => { + license.next(createUnavailableLicense()); + expect(() => + licenseState.ensureLicenseForAlertType(alertType) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert type test is disabled because license information is not available at this time."` + ); + }); + + test('should throw when license is expired', () => { + const expiredLicense = licensingMock.createLicense({ license: { status: 'expired' } }); + license.next(expiredLicense); + expect(() => + licenseState.ensureLicenseForAlertType(alertType) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert type test is disabled because your basic license has expired."` + ); + }); + + test('should throw when license is invalid', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + expect(() => + licenseState.ensureLicenseForAlertType(alertType) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert type test is disabled because your basic license does not support it. Please upgrade your license."` + ); + }); + + test('should not throw when license is valid', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForAlertType(alertType); + }); + + test('should call notifyUsage', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForAlertType(alertType); + expect(mockNotifyUsage).toHaveBeenCalledWith('Alert: Test'); + }); +}); + +function createUnavailableLicense() { + const unavailableLicense = licensingMock.createLicenseMock(); + unavailableLicense.isAvailable = false; + return unavailableLicense; +} diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index ead6b743f1719..7c2345bdc262b 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -6,10 +6,17 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { assertNever } from '@kbn/std'; import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../licensing/common/types'; +import { LicensingPluginStart } from '../../../licensing/server'; +import { ILicense, LicenseType } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; +import { getAlertTypeFeatureUsageName } from './get_alert_type_feature_usage_name'; +import { AlertType } from '../types'; +import { AlertTypeDisabledError } from './errors/alert_type_disabled'; + +export type ILicenseState = PublicMethodsOf; export interface AlertingLicenseInformation { showAppLink: boolean; @@ -20,12 +27,15 @@ export interface AlertingLicenseInformation { export class LicenseState { private licenseInformation: AlertingLicenseInformation = this.checkLicense(undefined); private subscription: Subscription; + private license?: ILicense; + private _notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage'] | null = null; constructor(license$: Observable) { this.subscription = license$.subscribe(this.updateInformation.bind(this)); } private updateInformation(license: ILicense | undefined) { + this.license = license; this.licenseInformation = this.checkLicense(license); } @@ -37,6 +47,47 @@ export class LicenseState { return this.licenseInformation; } + public setNotifyUsage(notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage']) { + this._notifyUsage = notifyUsage; + } + + public getLicenseCheckForAlertType( + alertTypeId: string, + alertTypeName: string, + minimumLicenseRequired: LicenseType, + { notifyUsage }: { notifyUsage: boolean } = { notifyUsage: false } + ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { + if (notifyUsage) { + this.notifyUsage(alertTypeName, minimumLicenseRequired); + } + + if (!this.license?.isAvailable) { + return { isValid: false, reason: 'unavailable' }; + } + + const check = this.license.check(alertTypeId, minimumLicenseRequired); + + switch (check.state) { + case 'expired': + return { isValid: false, reason: 'expired' }; + case 'invalid': + return { isValid: false, reason: 'invalid' }; + case 'unavailable': + return { isValid: false, reason: 'unavailable' }; + case 'valid': + return { isValid: true }; + default: + return assertNever(check.state); + } + } + + private notifyUsage(alertTypeName: string, minimumLicenseRequired: LicenseType) { + // No need to notify usage on basic alert types + if (this._notifyUsage && minimumLicenseRequired !== 'basic') { + this._notifyUsage(getAlertTypeFeatureUsageName(alertTypeName)); + } + } + public checkLicense(license: ILicense | undefined): AlertingLicenseInformation { if (!license || !license.isAvailable) { return { @@ -78,6 +129,53 @@ export class LicenseState { return assertNever(check.state); } } + + public ensureLicenseForAlertType(alertType: AlertType) { + this.notifyUsage(alertType.name, alertType.minimumLicenseRequired); + + const check = this.getLicenseCheckForAlertType( + alertType.id, + alertType.name, + alertType.minimumLicenseRequired + ); + + if (check.isValid) { + return; + } + switch (check.reason) { + case 'unavailable': + throw new AlertTypeDisabledError( + i18n.translate('xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage', { + defaultMessage: + 'Alert type {alertTypeId} is disabled because license information is not available at this time.', + values: { + alertTypeId: alertType.id, + }, + }), + 'license_unavailable' + ); + case 'expired': + throw new AlertTypeDisabledError( + i18n.translate('xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage', { + defaultMessage: + 'Alert type {alertTypeId} is disabled because your {licenseType} license has expired.', + values: { alertTypeId: alertType.id, licenseType: this.license!.type }, + }), + 'license_expired' + ); + case 'invalid': + throw new AlertTypeDisabledError( + i18n.translate('xpack.alerts.serverSideErrors.invalidLicenseErrorMessage', { + defaultMessage: + 'Alert type {alertTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.', + values: { alertTypeId: alertType.id, licenseType: this.license!.type }, + }), + 'license_invalid' + ); + default: + assertNever(check.reason); + } + } } export function verifyApiAccessFactory(licenseState: LicenseState) { diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index b41e70b9d6a5a..56d67d5b6f218 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -168,6 +168,7 @@ describe('Alerting Plugin', () => { }, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), features: mockFeatures(), + licensing: licensingMock.createStart(), } as unknown) as AlertingPluginsStart ); @@ -222,6 +223,7 @@ describe('Alerting Plugin', () => { }, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), features: mockFeatures(), + licensing: licensingMock.createStart(), } as unknown) as AlertingPluginsStart ); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 78129ffa3961a..63861f5050f25 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -19,7 +19,7 @@ import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry } from './alert_type_registry'; import { TaskRunnerFactory } from './task_runner'; import { AlertsClientFactory } from './alerts_client_factory'; -import { LicenseState } from './lib/license_state'; +import { ILicenseState, LicenseState } from './lib/license_state'; import { KibanaRequest, Logger, @@ -54,7 +54,7 @@ import { unmuteAlertInstanceRoute, healthRoute, } from './routes'; -import { LICENSE_TYPE, LicensingPluginSetup } from '../../licensing/server'; +import { LICENSE_TYPE, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as ActionsPluginSetupContract, PluginStartContract as ActionsPluginStartContract, @@ -130,6 +130,7 @@ export interface AlertingPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; features: FeaturesPluginStart; eventLog: IEventLogClientService; + licensing: LicensingPluginStart; spaces?: SpacesPluginStart; security?: SecurityPluginStart; } @@ -139,7 +140,7 @@ export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; - private licenseState: LicenseState | null = null; + private licenseState: ILicenseState | null = null; private isESOUsingEphemeralEncryptionKey?: boolean; private security?: SecurityPluginSetup; private readonly alertsClientFactory: AlertsClientFactory; @@ -197,6 +198,8 @@ export class AlertingPlugin { const alertTypeRegistry = new AlertTypeRegistry({ taskManager: plugins.taskManager, taskRunnerFactory: this.taskRunnerFactory, + licenseState: this.licenseState, + licensing: plugins.licensing, }); this.alertTypeRegistry = alertTypeRegistry; @@ -288,8 +291,11 @@ export class AlertingPlugin { alertTypeRegistry, alertsClientFactory, security, + licenseState, } = this; + licenseState?.setNotifyUsage(plugins.licensing.featureUsage.notifyUsage); + const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes: ['alert'], }); @@ -339,6 +345,7 @@ export class AlertingPlugin { basePathService: core.http.basePath, eventLogger: this.eventLogger!, internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), + alertTypeRegistry: this.alertTypeRegistry!, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerts/server/routes/aggregate.test.ts b/x-pack/plugins/alerts/server/routes/aggregate.test.ts index 498ee7ba2da58..199c336dd977d 100644 --- a/x-pack/plugins/alerts/server/routes/aggregate.test.ts +++ b/x-pack/plugins/alerts/server/routes/aggregate.test.ts @@ -6,7 +6,7 @@ import { aggregateAlertRoute } from './aggregate'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('aggregateAlertRoute', () => { it('aggregate alerts with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); aggregateAlertRoute(router, licenseState); @@ -84,7 +84,7 @@ describe('aggregateAlertRoute', () => { }); it('ensures the license allows aggregating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); aggregateAlertRoute(router, licenseState); @@ -116,7 +116,7 @@ describe('aggregateAlertRoute', () => { }); it('ensures the license check prevents aggregating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/alerts/server/routes/aggregate.ts b/x-pack/plugins/alerts/server/routes/aggregate.ts index 2c36521b07269..0fcfb6f6147e7 100644 --- a/x-pack/plugins/alerts/server/routes/aggregate.ts +++ b/x-pack/plugins/alerts/server/routes/aggregate.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { renameKeys } from './lib/rename_keys'; @@ -38,7 +38,7 @@ const querySchema = schema.object({ filter: schema.maybe(schema.string()), }); -export const aggregateAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const aggregateAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/_aggregate`, diff --git a/x-pack/plugins/alerts/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts index 51c5d2525631d..c9ad7937598a3 100644 --- a/x-pack/plugins/alerts/server/routes/create.test.ts +++ b/x-pack/plugins/alerts/server/routes/create.test.ts @@ -6,11 +6,12 @@ import { createAlertRoute } from './create'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; import { Alert } from '../../common/alert'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); @@ -72,7 +73,7 @@ describe('createAlertRoute', () => { }; it('creates an alert with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -131,7 +132,7 @@ describe('createAlertRoute', () => { }); it('ensures the license allows creating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -148,7 +149,7 @@ describe('createAlertRoute', () => { }); it('ensures the license check prevents creating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -167,4 +168,21 @@ describe('createAlertRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.create.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}, ['ok', 'forbidden']); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts index 91a81f6d84b71..d9678b187a84d 100644 --- a/x-pack/plugins/alerts/server/routes/create.ts +++ b/x-pack/plugins/alerts/server/routes/create.ts @@ -12,11 +12,12 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { validateDurationSchema } from '../lib'; import { handleDisabledApiKeysError } from './lib/error_handler'; import { Alert, BASE_ALERT_API_PATH } from '../types'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; export const bodySchema = schema.object({ name: schema.string(), @@ -40,7 +41,7 @@ export const bodySchema = schema.object({ ), }); -export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const createAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert`, @@ -61,10 +62,17 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => } const alertsClient = context.alerting.getAlertsClient(); const alert = req.body; - const alertRes: Alert = await alertsClient.create({ data: alert }); - return res.ok({ - body: alertRes, - }); + try { + const alertRes: Alert = await alertsClient.create({ data: alert }); + return res.ok({ + body: alertRes, + }); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ) ); diff --git a/x-pack/plugins/alerts/server/routes/delete.test.ts b/x-pack/plugins/alerts/server/routes/delete.test.ts index d9c5aa2d59c87..e704ed498fc0c 100644 --- a/x-pack/plugins/alerts/server/routes/delete.test.ts +++ b/x-pack/plugins/alerts/server/routes/delete.test.ts @@ -5,7 +5,7 @@ */ import { deleteAlertRoute } from './delete'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('deleteAlertRoute', () => { it('deletes an alert with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -58,7 +58,7 @@ describe('deleteAlertRoute', () => { }); it('ensures the license allows deleting alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -80,7 +80,7 @@ describe('deleteAlertRoute', () => { }); it('ensures the license check prevents deleting alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/alerts/server/routes/delete.ts b/x-pack/plugins/alerts/server/routes/delete.ts index b073c59149171..3ac975d3a1546 100644 --- a/x-pack/plugins/alerts/server/routes/delete.ts +++ b/x-pack/plugins/alerts/server/routes/delete.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; @@ -20,7 +20,7 @@ const paramSchema = schema.object({ id: schema.string(), }); -export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const deleteAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.delete( { path: `${BASE_ALERT_API_PATH}/alert/{id}`, diff --git a/x-pack/plugins/alerts/server/routes/disable.test.ts b/x-pack/plugins/alerts/server/routes/disable.test.ts index 74f7b2eb8a570..4e736eb315d35 100644 --- a/x-pack/plugins/alerts/server/routes/disable.test.ts +++ b/x-pack/plugins/alerts/server/routes/disable.test.ts @@ -6,9 +6,10 @@ import { disableAlertRoute } from './disable'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); @@ -22,7 +23,7 @@ beforeEach(() => { describe('disableAlertRoute', () => { it('disables an alert', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); disableAlertRoute(router, licenseState); @@ -56,4 +57,24 @@ describe('disableAlertRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + disableAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.disable.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/disable.ts b/x-pack/plugins/alerts/server/routes/disable.ts index 234f8ed959a5d..e96cb397f554b 100644 --- a/x-pack/plugins/alerts/server/routes/disable.ts +++ b/x-pack/plugins/alerts/server/routes/disable.ts @@ -12,15 +12,16 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), }); -export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const disableAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_disable`, @@ -39,8 +40,15 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - await alertsClient.disable({ id }); - return res.noContent(); + try { + await alertsClient.disable({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/alerts/server/routes/enable.test.ts b/x-pack/plugins/alerts/server/routes/enable.test.ts index c9575ef87f767..8db0f2ae68938 100644 --- a/x-pack/plugins/alerts/server/routes/enable.test.ts +++ b/x-pack/plugins/alerts/server/routes/enable.test.ts @@ -5,9 +5,10 @@ */ import { enableAlertRoute } from './enable'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); @@ -21,7 +22,7 @@ beforeEach(() => { describe('enableAlertRoute', () => { it('enables an alert', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); enableAlertRoute(router, licenseState); @@ -55,4 +56,24 @@ describe('enableAlertRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + enableAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.enable.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/enable.ts b/x-pack/plugins/alerts/server/routes/enable.ts index c162b4a9844b3..81c5027c7587b 100644 --- a/x-pack/plugins/alerts/server/routes/enable.ts +++ b/x-pack/plugins/alerts/server/routes/enable.ts @@ -12,16 +12,17 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { handleDisabledApiKeysError } from './lib/error_handler'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), }); -export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const enableAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_enable`, @@ -41,8 +42,15 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - await alertsClient.enable({ id }); - return res.noContent(); + try { + await alertsClient.enable({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ) ); diff --git a/x-pack/plugins/alerts/server/routes/find.test.ts b/x-pack/plugins/alerts/server/routes/find.test.ts index 46702f96a2e10..c6c98ca662712 100644 --- a/x-pack/plugins/alerts/server/routes/find.test.ts +++ b/x-pack/plugins/alerts/server/routes/find.test.ts @@ -6,7 +6,7 @@ import { findAlertRoute } from './find'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('findAlertRoute', () => { it('finds alerts with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -82,7 +82,7 @@ describe('findAlertRoute', () => { }); it('ensures the license allows finding alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -113,7 +113,7 @@ describe('findAlertRoute', () => { }); it('ensures the license check prevents finding alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/alerts/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts index ef3b16dc9e517..487ff571187f4 100644 --- a/x-pack/plugins/alerts/server/routes/find.ts +++ b/x-pack/plugins/alerts/server/routes/find.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { renameKeys } from './lib/rename_keys'; @@ -43,7 +43,7 @@ const querySchema = schema.object({ filter: schema.maybe(schema.string()), }); -export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const findAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/_find`, diff --git a/x-pack/plugins/alerts/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts index c60177e90b79d..f8c66d4c6561f 100644 --- a/x-pack/plugins/alerts/server/routes/get.test.ts +++ b/x-pack/plugins/alerts/server/routes/get.test.ts @@ -6,7 +6,7 @@ import { getAlertRoute } from './get'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -59,7 +59,7 @@ describe('getAlertRoute', () => { }; it('gets an alert with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -87,7 +87,7 @@ describe('getAlertRoute', () => { }); it('ensures the license allows getting alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -110,7 +110,7 @@ describe('getAlertRoute', () => { }); it('ensures the license check prevents getting alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/alerts/server/routes/get.ts b/x-pack/plugins/alerts/server/routes/get.ts index 0f3fc4b2f3e41..ae592f37cd55c 100644 --- a/x-pack/plugins/alerts/server/routes/get.ts +++ b/x-pack/plugins/alerts/server/routes/get.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; @@ -20,7 +20,7 @@ const paramSchema = schema.object({ id: schema.string(), }); -export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const getAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/alert/{id}`, diff --git a/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts index 8957a3d7c091e..eb0d3ad480eec 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts @@ -6,7 +6,7 @@ import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; @@ -40,7 +40,7 @@ describe('getAlertInstanceSummaryRoute', () => { }; it('gets alert instance summary', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertInstanceSummaryRoute(router, licenseState); @@ -78,7 +78,7 @@ describe('getAlertInstanceSummaryRoute', () => { }); it('returns NOT-FOUND when alert is not found', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertInstanceSummaryRoute(router, licenseState); diff --git a/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts index 11a10c2967a58..33f331f7dce02 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; @@ -24,7 +24,7 @@ const querySchema = schema.object({ dateStart: schema.maybe(schema.string()), }); -export const getAlertInstanceSummaryRoute = (router: IRouter, licenseState: LicenseState) => { +export const getAlertInstanceSummaryRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_instance_summary`, diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts index d5bf9737d39ab..a3d0a93b34998 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts @@ -6,7 +6,7 @@ import { getAlertStateRoute } from './get_alert_state'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; @@ -40,7 +40,7 @@ describe('getAlertStateRoute', () => { }; it('gets alert state', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -76,7 +76,7 @@ describe('getAlertStateRoute', () => { }); it('returns NO-CONTENT when alert exists but has no task state yet', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -112,7 +112,7 @@ describe('getAlertStateRoute', () => { }); it('returns NOT-FOUND when alert is not found', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.ts index 089fc80fca355..52ad8f9f31874 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.ts @@ -12,7 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; @@ -20,7 +20,7 @@ const paramSchema = schema.object({ id: schema.string(), }); -export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) => { +export const getAlertStateRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/alert/{id}/state`, diff --git a/x-pack/plugins/alerts/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts index d1967c6dd9bf8..2361f0c90e031 100644 --- a/x-pack/plugins/alerts/server/routes/health.test.ts +++ b/x-pack/plugins/alerts/server/routes/health.test.ts @@ -9,7 +9,7 @@ import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { alertsClientMock } from '../alerts_client.mock'; import { HealthStatus } from '../types'; @@ -45,7 +45,7 @@ describe('healthRoute', () => { it('registers the route', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -58,7 +58,7 @@ describe('healthRoute', () => { it('queries the usage api', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -87,7 +87,7 @@ describe('healthRoute', () => { it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = true; healthRoute(router, licenseState, encryptedSavedObjects); @@ -127,7 +127,7 @@ describe('healthRoute', () => { it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -167,7 +167,7 @@ describe('healthRoute', () => { it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -207,7 +207,7 @@ describe('healthRoute', () => { it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -247,7 +247,7 @@ describe('healthRoute', () => { it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); @@ -289,7 +289,7 @@ describe('healthRoute', () => { it('evaluates security and tls enabled to mean that the user can generate keys', async () => { const router = httpServiceMock.createRouter(); - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); encryptedSavedObjects.usingEphemeralEncryptionKey = false; healthRoute(router, licenseState, encryptedSavedObjects); diff --git a/x-pack/plugins/alerts/server/routes/health.ts b/x-pack/plugins/alerts/server/routes/health.ts index bfd5b1e272287..962ad7e1bb29a 100644 --- a/x-pack/plugins/alerts/server/routes/health.ts +++ b/x-pack/plugins/alerts/server/routes/health.ts @@ -11,7 +11,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { AlertingFrameworkHealth } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; @@ -29,7 +29,7 @@ interface XPackUsageSecurity { export function healthRoute( router: IRouter, - licenseState: LicenseState, + licenseState: ILicenseState, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ) { router.get( diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 33c6a1d81139c..86baaf86b2d4f 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -6,11 +6,12 @@ import { listAlertTypesRoute } from './list_alert_types'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; import { RecoveredActionGroup } from '../../common'; +import { RegistryAlertTypeWithAuth } from '../authorization'; const alertsClient = alertsClientMock.create(); @@ -24,7 +25,7 @@ beforeEach(() => { describe('listAlertTypesRoute', () => { it('lists alert types with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -52,7 +53,8 @@ describe('listAlertTypesRoute', () => { state: [], }, producer: 'test', - }, + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, ]; alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); @@ -74,6 +76,7 @@ describe('listAlertTypesRoute', () => { }, "authorizedConsumers": Object {}, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "1", "minimumLicenseRequired": "basic", "name": "name", @@ -95,7 +98,7 @@ describe('listAlertTypesRoute', () => { }); it('ensures the license allows listing alert types', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -123,7 +126,8 @@ describe('listAlertTypesRoute', () => { state: [], }, producer: 'alerts', - }, + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, ]; alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); @@ -142,7 +146,7 @@ describe('listAlertTypesRoute', () => { }); it('ensures the license check prevents listing alert types', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -174,7 +178,8 @@ describe('listAlertTypesRoute', () => { state: [], }, producer: 'alerts', - }, + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, ]; alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.ts index bf516120fbe93..9b4b352e211f1 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.ts @@ -11,11 +11,11 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; -export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) => { +export const listAlertTypesRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `${BASE_ALERT_API_PATH}/list_alert_types`, diff --git a/x-pack/plugins/alerts/server/routes/mute_all.test.ts b/x-pack/plugins/alerts/server/routes/mute_all.test.ts index efa3cdebad8ff..2599672e02fb4 100644 --- a/x-pack/plugins/alerts/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.test.ts @@ -6,9 +6,10 @@ import { muteAllAlertRoute } from './mute_all'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -21,7 +22,7 @@ beforeEach(() => { describe('muteAllAlertRoute', () => { it('mute an alert', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); muteAllAlertRoute(router, licenseState); @@ -55,4 +56,24 @@ describe('muteAllAlertRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAllAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.muteAll.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/mute_all.ts b/x-pack/plugins/alerts/server/routes/mute_all.ts index 6735121d4edb0..224216961bb7f 100644 --- a/x-pack/plugins/alerts/server/routes/mute_all.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.ts @@ -12,15 +12,16 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), }); -export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const muteAllAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, @@ -39,8 +40,15 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - await alertsClient.muteAll({ id }); - return res.noContent(); + try { + await alertsClient.muteAll({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts index 6e700e4e3fd46..cdfe4c5a80f8a 100644 --- a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts @@ -6,9 +6,10 @@ import { muteAlertInstanceRoute } from './mute_instance'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -21,7 +22,7 @@ beforeEach(() => { describe('muteAlertInstanceRoute', () => { it('mutes an alert instance', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); muteAlertInstanceRoute(router, licenseState); @@ -59,4 +60,26 @@ describe('muteAlertInstanceRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + muteAlertInstanceRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.muteInstance.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.ts b/x-pack/plugins/alerts/server/routes/mute_instance.ts index 5e2ffc7d519ed..b374866177231 100644 --- a/x-pack/plugins/alerts/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.ts @@ -12,18 +12,19 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { renameKeys } from './lib/rename_keys'; import { MuteOptions } from '../alerts_client'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ alert_id: schema.string(), alert_instance_id: schema.string(), }); -export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { +export const muteAlertInstanceRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, @@ -48,8 +49,15 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta }; const renamedQuery = renameKeys>(renameMap, req.params); - await alertsClient.muteInstance(renamedQuery); - return res.noContent(); + try { + await alertsClient.muteInstance(renamedQuery); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts index 81fdc5bb4dd76..b58d34f25324c 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts @@ -5,9 +5,10 @@ */ import { unmuteAllAlertRoute } from './unmute_all'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -20,7 +21,7 @@ beforeEach(() => { describe('unmuteAllAlertRoute', () => { it('unmutes an alert', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); unmuteAllAlertRoute(router, licenseState); @@ -54,4 +55,24 @@ describe('unmuteAllAlertRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAllAlertRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.unmuteAll.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.ts b/x-pack/plugins/alerts/server/routes/unmute_all.ts index a987380541696..e249ec7ffa58f 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.ts @@ -12,15 +12,16 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), }); -export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const unmuteAllAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, @@ -39,8 +40,15 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - await alertsClient.unmuteAll({ id }); - return res.noContent(); + try { + await alertsClient.unmuteAll({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts index 04e97dbe5e538..96985c489d3f5 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts @@ -6,9 +6,10 @@ import { unmuteAlertInstanceRoute } from './unmute_instance'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -21,7 +22,7 @@ beforeEach(() => { describe('unmuteAlertInstanceRoute', () => { it('unmutes an alert instance', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); unmuteAlertInstanceRoute(router, licenseState); @@ -59,4 +60,26 @@ describe('unmuteAlertInstanceRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unmuteAlertInstanceRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.unmuteInstance.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.ts index 15b882e585804..bcab6e21578aa 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.ts @@ -12,16 +12,17 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ alertId: schema.string(), alertInstanceId: schema.string(), }); -export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { +export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, @@ -40,8 +41,15 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS } const alertsClient = context.alerting.getAlertsClient(); const { alertId, alertInstanceId } = req.params; - await alertsClient.unmuteInstance({ alertId, alertInstanceId }); - return res.noContent(); + try { + await alertsClient.unmuteInstance({ alertId, alertInstanceId }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/alerts/server/routes/update.test.ts b/x-pack/plugins/alerts/server/routes/update.test.ts index dedb08a9972c2..9394b1958c7d1 100644 --- a/x-pack/plugins/alerts/server/routes/update.test.ts +++ b/x-pack/plugins/alerts/server/routes/update.test.ts @@ -6,10 +6,11 @@ import { updateAlertRoute } from './update'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -44,7 +45,7 @@ describe('updateAlertRoute', () => { }; it('updates an alert with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -120,7 +121,7 @@ describe('updateAlertRoute', () => { }); it('ensures the license allows updating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -163,7 +164,7 @@ describe('updateAlertRoute', () => { }); it('ensures the license check prevents updating alerts', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -208,4 +209,24 @@ describe('updateAlertRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateAlertRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + alertsClient.update.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/update.ts b/x-pack/plugins/alerts/server/routes/update.ts index 9b2fe9a43810b..33fd3c4c61afd 100644 --- a/x-pack/plugins/alerts/server/routes/update.ts +++ b/x-pack/plugins/alerts/server/routes/update.ts @@ -12,11 +12,12 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { validateDurationSchema } from '../lib'; import { handleDisabledApiKeysError } from './lib/error_handler'; import { BASE_ALERT_API_PATH } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -41,7 +42,7 @@ const bodySchema = schema.object({ ), }); -export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const updateAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.put( { path: `${BASE_ALERT_API_PATH}/alert/{id}`, @@ -63,12 +64,20 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; const { name, actions, params, schedule, tags, throttle } = req.body; - return res.ok({ - body: await alertsClient.update({ + try { + const alertRes = await alertsClient.update({ id, data: { name, actions, params, schedule, tags, throttle }, - }), - }); + }); + return res.ok({ + body: alertRes, + }); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ) ); diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts index 5aa91d215be90..13bd341af2232 100644 --- a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts @@ -6,9 +6,10 @@ import { updateApiKeyRoute } from './update_api_key'; import { httpServiceMock } from 'src/core/server/mocks'; -import { mockLicenseState } from '../lib/license_state.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -21,7 +22,7 @@ beforeEach(() => { describe('updateApiKeyRoute', () => { it('updates api key for an alert', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); updateApiKeyRoute(router, licenseState); @@ -55,4 +56,26 @@ describe('updateApiKeyRoute', () => { expect(res.noContent).toHaveBeenCalled(); }); + + it('ensures the alert type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateApiKeyRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + alertsClient.updateApiKey.mockRejectedValue( + new AlertTypeDisabledError('Fail', 'license_invalid') + ); + + const [context, req, res] = mockHandlerArguments({ alertsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.ts b/x-pack/plugins/alerts/server/routes/update_api_key.ts index d44649b05b929..fb7639d975980 100644 --- a/x-pack/plugins/alerts/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.ts @@ -12,16 +12,17 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; +import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { handleDisabledApiKeysError } from './lib/error_handler'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), }); -export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) => { +export const updateApiKeyRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, @@ -41,8 +42,15 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - await alertsClient.updateApiKey({ id }); - return res.noContent(); + try { + await alertsClient.updateApiKey({ id }); + return res.noContent(); + } catch (e) { + if (e instanceof AlertTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } }) ) ); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index e48a312fa920e..f80b437b5391f 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -29,6 +29,7 @@ import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; import { NormalizedAlertType } from '../alert_type_registry'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; const alertType = { id: 'test', name: 'My test alert', @@ -72,6 +73,7 @@ describe('Task Runner', () => { const services = alertsMock.createAlertServices(); const actionsClient = actionsClientMock.create(); const alertsClient = alertsClientMock.create(); + const alertTypeRegistry = alertTypeRegistryMock.create(); const taskRunnerFactoryInitializerParams: jest.Mocked & { actionsPlugin: jest.Mocked; @@ -86,6 +88,7 @@ describe('Task Runner', () => { basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), + alertTypeRegistry, }; const mockedAlertTypeSavedObject: Alert = { @@ -384,7 +387,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -494,7 +497,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1277,6 +1280,73 @@ describe('Task Runner', () => { `); }); + test('recovers gracefully when the Alert Task Runner throws an exception when license is higher than supported', async () => { + alertTypeRegistry.ensureAlertTypeEnabled.mockImplementation(() => { + throw new Error('OMG'); + }); + + const taskRunner = new TaskRunner( + alertType as NormalizedAlertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult).toMatchInlineSnapshot(` + Object { + "schedule": Object { + "interval": "10s", + }, + "state": Object {}, + } + `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "license", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); + }); + test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { taskRunnerFactoryInitializerParams.getServices.mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 0c486dad070ef..d38ad4ea216a0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -30,6 +30,7 @@ import { SanitizedAlert, AlertExecutionStatus, AlertExecutionStatusErrorReasons, + AlertTypeRegistry, } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; @@ -59,6 +60,7 @@ export class TaskRunner { private logger: Logger; private taskInstance: AlertTaskInstance; private alertType: NormalizedAlertType; + private readonly alertTypeRegistry: AlertTypeRegistry; constructor( alertType: NormalizedAlertType, @@ -69,6 +71,7 @@ export class TaskRunner { this.logger = context.logger; this.alertType = alertType; this.taskInstance = taskInstanceToAlertTaskInstance(taskInstance); + this.alertTypeRegistry = context.alertTypeRegistry; } async getApiKeyForAlertPermissions(alertId: string, spaceId: string) { @@ -336,6 +339,11 @@ export class TaskRunner { throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, err); } + try { + this.alertTypeRegistry.ensureAlertTypeEnabled(alert.alertTypeId); + } catch (err) { + throw new ErrorWithReason(AlertExecutionStatusErrorReasons.License, err); + } return { state: await promiseResult( this.validateAndExecuteAlert(services, apiKey, alert, event) diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index f58939440212e..6c58b64fffa92 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -17,6 +17,7 @@ import { actionsMock } from '../../../actions/server/mocks'; import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { NormalizedAlertType } from '../alert_type_registry'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; const alertType: NormalizedAlertType = { id: 'test', @@ -74,6 +75,7 @@ describe('Task Runner Factory', () => { basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), + alertTypeRegistry: alertTypeRegistryMock.create(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index 405afbf53c075..1fe94972bd4b0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -13,7 +13,7 @@ import { import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; -import { GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; +import { AlertTypeRegistry, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; import { AlertsClient } from '../alerts_client'; @@ -29,6 +29,7 @@ export interface TaskRunnerContext { spaceIdToNamespace: SpaceIdToNamespaceFunction; basePathService: IBasePath; internalSavedObjectsRepository: ISavedObjectsRepository; + alertTypeRegistry: AlertTypeRegistry; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts index dbcf2d6854af5..6b2dd29ffd8aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts @@ -77,9 +77,17 @@ export const ALERT_ERROR_EXECUTION_REASON = i18n.translate( } ); +export const ALERT_ERROR_LICENSE_REASON = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning', + { + defaultMessage: 'An error occurred when running the alert.', + } +); + export const alertsErrorReasonTranslationsMapping = { read: ALERT_ERROR_READING_REASON, decrypt: ALERT_ERROR_DECRYPTING_REASON, execute: ALERT_ERROR_EXECUTION_REASON, unknown: ALERT_ERROR_UNKNOWN_REASON, + license: ALERT_ERROR_LICENSE_REASON, }; diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts new file mode 100644 index 0000000000000..f6b0ef2a773f1 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function basicAlertTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('basic alert', () => { + it('should return 200 when creating a basic license alert', async () => { + await supertest + .post(`/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts new file mode 100644 index 0000000000000..609a71b51dd5b --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function emailTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('create gold noop alert', () => { + it('should return 403 when creating an gold alert', async () => { + await supertest + .post(`/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ alertTypeId: 'test.gold.noop' })) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Alert type test.gold.noop is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/index.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/index.ts new file mode 100644 index 0000000000000..84fceb9a6c0f4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('Alerts', () => { + loadTestFile(require.resolve('./gold_noop_alert_type')); + loadTestFile(require.resolve('./basic_noop_alert_type')); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/index.ts b/x-pack/test/alerting_api_integration/basic/tests/index.ts index 7f3152cc38ca8..80152cca07c60 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/index.ts @@ -15,5 +15,6 @@ export default function alertingApiIntegrationTests({ this.tags('ciGroup3'); loadTestFile(require.resolve('./actions')); + loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index f4928d5fc60d3..55d0320085b82 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -410,6 +410,15 @@ export function defineAlertTypes( minimumLicenseRequired: 'basic', async executor() {}, }; + const goldNoopAlertType: AlertType = { + id: 'test.gold.noop', + name: 'Test: Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'gold', + async executor() {}, + }; const onlyContextVariablesAlertType: AlertType = { id: 'test.onlyContextVariables', name: 'Test: Only Context Variables', @@ -479,4 +488,5 @@ export function defineAlertTypes( alerts.registerType(getPatternFiringAlertType()); alerts.registerType(throwAlertType); alerts.registerType(longRunningAlertType); + alerts.registerType(goldNoopAlertType); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 3ec85d558d4e8..87cc355a58568 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -33,6 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { id: 'recovered', name: 'Recovered', }, + enabledInLicense: true, }; const expectedRestrictedNoOpType = { @@ -54,6 +55,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }, producer: 'alertsRestrictedFixture', minimumLicenseRequired: 'basic', + enabledInLicense: true, }; describe('list_alert_types', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 0b27adf27efdd..74deaf4c7296f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -41,6 +41,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }, producer: 'alertsFixture', minimumLicenseRequired: 'basic', + enabledInLicense: true, }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); });