diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index fdaf8edab3a33..4fde4183b414e 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -49,6 +49,7 @@ export const alertType: AlertType< { id: 'large', name: 'Large t-shirt' }, ], defaultActionGroupId: DEFAULT_ACTION_GROUP, + minimumLicenseRequired: 'basic', async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds }, diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index f0f47adffa109..27a8bfc7a53a3 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -42,6 +42,7 @@ export const alertType: AlertType = { id: 'example.people-in-space', name: 'People In Space Right Now', actionGroups: [{ id: 'default', name: 'default' }], + minimumLicenseRequired: 'basic', defaultActionGroupId: 'default', recoveryActionGroup: { id: 'hasLandedBackOnEarth', 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/README.md b/x-pack/plugins/alerts/README.md index 5bd47c287b891..39dc23c7bbb73 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -172,6 +172,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ { name: 'cpuUsage', description: 'CPU usage' }, ], }, + minimumLicenseRequired: 'basic', async executor({ alertId, startedAt, @@ -239,6 +240,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', actionVariables: { context: [ { name: 'server', description: 'the server' }, diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index e0e73e978f775..d74f66898eff6 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -26,6 +26,7 @@ export enum AlertExecutionStatusErrorReasons { Decrypt = 'decrypt', Execute = 'execute', Unknown = 'unknown', + License = 'license', } export interface AlertExecutionStatus { diff --git a/x-pack/plugins/alerts/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts index a06c6d2fd5af2..4ab3ddc7ca810 100644 --- a/x-pack/plugins/alerts/common/alert_type.ts +++ b/x-pack/plugins/alerts/common/alert_type.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LicenseType } from '../../licensing/common/types'; + export interface AlertType { id: string; name: string; @@ -12,6 +14,7 @@ export interface AlertType { actionVariables: string[]; defaultActionGroupId: ActionGroup['id']; producer: string; + minimumLicenseRequired: LicenseType; } export interface ActionGroup { diff --git a/x-pack/plugins/alerts/public/alert_api.test.ts b/x-pack/plugins/alerts/public/alert_api.test.ts index 03c55dfdf5b28..bfa7065473dc6 100644 --- a/x-pack/plugins/alerts/public/alert_api.test.ts +++ b/x-pack/plugins/alerts/public/alert_api.test.ts @@ -22,6 +22,7 @@ describe('loadAlertTypes', () => { actionVariables: ['var1'], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', }, @@ -46,6 +47,7 @@ describe('loadAlertType', () => { actionVariables: ['var1'], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', }; @@ -67,6 +69,7 @@ describe('loadAlertType', () => { actionVariables: [], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', }; @@ -83,6 +86,7 @@ describe('loadAlertType', () => { actionVariables: [], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', }, diff --git a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts index bf005e07f959e..9b40964f71949 100644 --- a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts @@ -18,6 +18,7 @@ const mockAlertType = (id: string): AlertType => ({ actionVariables: [], defaultActionGroupId: 'default', producer: 'alerts', + minimumLicenseRequired: 'basic', }); describe('AlertNavigationRegistry', () => { 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 e4811daa3611b..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', () => { @@ -35,6 +45,7 @@ describe('has()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }); @@ -44,7 +55,7 @@ describe('has()', () => { describe('register()', () => { test('throws if AlertType Id contains invalid characters', () => { - const alertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -54,6 +65,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -75,7 +87,7 @@ describe('register()', () => { }); test('throws if AlertType Id isnt a string', () => { - const alertType = { + const alertType: AlertType = { id: (123 as unknown) as string, name: 'Test', actionGroups: [ @@ -85,6 +97,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -96,7 +109,7 @@ describe('register()', () => { }); test('throws if AlertType action groups contains reserved group id', () => { - const alertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -110,6 +123,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -123,7 +137,7 @@ describe('register()', () => { }); test('allows an AlertType to specify a custom recovery group', () => { - const alertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -139,6 +153,7 @@ describe('register()', () => { }, executor: jest.fn(), producer: 'alerts', + minimumLicenseRequired: 'basic', }; const registry = new AlertTypeRegistry(alertTypeRegistryParams); registry.register(alertType); @@ -157,7 +172,7 @@ describe('register()', () => { }); test('throws if the custom recovery group is contained in the AlertType action groups', () => { - const alertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -175,6 +190,7 @@ describe('register()', () => { name: 'Back To Awesome', }, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -188,7 +204,7 @@ describe('register()', () => { }); test('registers the executor with the task manager', () => { - const alertType = { + const alertType: AlertType = { id: 'test', name: 'Test', actionGroups: [ @@ -198,6 +214,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -227,6 +244,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }; @@ -248,6 +266,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }); @@ -262,6 +281,7 @@ describe('register()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }) @@ -282,6 +302,7 @@ describe('get()', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }); @@ -306,6 +327,7 @@ describe('get()', () => { "defaultActionGroupId": "default", "executor": [MockFunction], "id": "test", + "minimumLicenseRequired": "basic", "name": "Test", "producer": "alerts", "recoveryActionGroup": Object { @@ -343,6 +365,7 @@ describe('list()', () => { }, ], defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', }); @@ -366,7 +389,9 @@ describe('list()', () => { "state": Array [], }, "defaultActionGroupId": "testActionGroup", + "enabledInLicense": false, "id": "test", + "minimumLicenseRequired": "basic", "name": "Test", "producer": "alerts", "recoveryActionGroup": Object { @@ -413,12 +438,50 @@ 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 = { + const baseAlert: AlertType = { id, name: `${id}-name`, actionGroups: [], defaultActionGroupId: id, + minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', }; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index a3e80fbd6c11a..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< @@ -146,6 +168,7 @@ export class AlertTypeRegistry { defaultActionGroupId, actionVariables, producer, + minimumLicenseRequired, }, ]: [string, NormalizedAlertType]) => ({ id, @@ -155,6 +178,12 @@ export class AlertTypeRegistry { defaultActionGroupId, 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 b1696696b3044..095823952722b 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -242,6 +242,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); @@ -653,6 +655,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); + const updateResult = await this.updateAlert({ id, data }, alertSavedObject); await Promise.all([ @@ -830,6 +834,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { @@ -913,6 +919,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + if (attributes.enabled === false) { const username = await this.getUserName(); const updateAttributes = this.updateMeta({ @@ -1012,6 +1020,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', @@ -1086,6 +1096,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const updateAttributes = this.updateMeta({ muteAll: true, mutedInstanceIds: [], @@ -1145,6 +1157,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const updateAttributes = this.updateMeta({ muteAll: false, mutedInstanceIds: [], @@ -1204,6 +1218,8 @@ export class AlertsClient { }) ); + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); @@ -1268,6 +1284,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 b21e3dcdf563d..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,15 +50,17 @@ beforeEach(() => { setGlobalDate(); describe('aggregate()', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -104,11 +107,13 @@ describe('aggregate()', () => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', 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 4e273ee3a9e44..5f830a6c5bc51 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 @@ -1186,6 +1186,7 @@ describe('create()', () => { threshold: schema.number({ min: 0, max: 1 }), }), }, + minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', }); @@ -1622,4 +1623,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 ff64150dc2b79..0efc8782e84c0 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,15 +54,17 @@ beforeEach(() => { setGlobalDate(); describe('find()', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], recoveryActionGroup: RecoveredActionGroup, actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -116,10 +119,12 @@ describe('find()', () => { actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup: RecoveredActionGroup, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts index 8f692cf548a9a..9be1e39fb3e05 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -85,6 +85,7 @@ export function getBeforeSetup( actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup: RecoveredActionGroup, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', })); 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 f3521965d615d..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,23 +51,27 @@ beforeEach(() => { describe('listAlertTypes', () => { let alertsClient: AlertsClient; - const alertingAlertType = { + const alertingAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'alertingAlertType', name: 'alertingAlertType', producer: 'alerts', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); @@ -80,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 }, ]) @@ -94,23 +102,27 @@ describe('listAlertTypes', () => { }); describe('authorization', () => { - const listedTypes = new Set([ + const listedTypes = new Set([ { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myType', name: 'myType', producer: 'myApp', + enabledInLicense: true, }, { id: 'myOtherType', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', + enabledInLicense: true, }, ]); beforeEach(() => { @@ -118,17 +130,19 @@ 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', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]); authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 42cec57b555de..3396a9c73e367 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -103,6 +103,7 @@ describe('update()', () => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', @@ -695,6 +696,7 @@ describe('update()', () => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, validate: { params: schema.object({ @@ -1045,6 +1047,7 @@ describe('update()', () => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts index aaa70a2594a5e..c5fba397bdb8e 100644 --- a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -333,6 +333,7 @@ beforeEach(() => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', @@ -343,6 +344,7 @@ beforeEach(() => { name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', 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 ccc325d468c54..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(); @@ -173,6 +174,7 @@ beforeEach(() => { name: 'My Alert Type', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'myApp', @@ -532,32 +534,38 @@ describe('AlertsAuthorization', () => { }); describe('getFindAuthorizationFilter', () => { - const myOtherAppAlertType = { + const myOtherAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', producer: 'alerts', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; - const mySecondAppAlertType = { + const mySecondAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); @@ -825,23 +833,27 @@ describe('AlertsAuthorization', () => { }); describe('filterByAlertTypeAuthorization', () => { - const myOtherAppAlertType = { + const myOtherAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', producer: 'myOtherApp', + enabledInLicense: true, }; - const myAppAlertType = { + const myAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); @@ -884,7 +896,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", + "minimumLicenseRequired": "basic", "name": "myAppAlertType", "producer": "myApp", "recoveryActionGroup": Object { @@ -914,7 +928,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", "producer": "myOtherApp", "recoveryActionGroup": Object { @@ -984,7 +1000,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", "producer": "myOtherApp", "recoveryActionGroup": Object { @@ -1010,7 +1028,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", + "minimumLicenseRequired": "basic", "name": "myAppAlertType", "producer": "myApp", "recoveryActionGroup": Object { @@ -1075,7 +1095,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", + "minimumLicenseRequired": "basic", "name": "myAppAlertType", "producer": "myApp", "recoveryActionGroup": Object { @@ -1169,7 +1191,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", "producer": "myOtherApp", "recoveryActionGroup": Object { @@ -1195,7 +1219,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myAppAlertType", + "minimumLicenseRequired": "basic", "name": "myAppAlertType", "producer": "myApp", "recoveryActionGroup": Object { @@ -1273,7 +1299,9 @@ describe('AlertsAuthorization', () => { }, }, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", "name": "myOtherAppAlertType", "producer": "myOtherApp", "recoveryActionGroup": Object { 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 4be52f12da9c7..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 @@ -22,9 +22,11 @@ describe('asFiltersByAlertTypeAndConsumer', () => { id: 'myAppAlertType', name: 'myAppAlertType', producer: 'myApp', + minimumLicenseRequired: 'basic', authorizedConsumers: { myApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ) @@ -42,6 +44,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { { actionGroups: [], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', @@ -51,6 +54,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myApp: { read: true, all: true }, myOtherApp: { read: true, all: true }, }, + enabledInLicense: true, }, ]) ) @@ -68,6 +72,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => { { actionGroups: [], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', @@ -78,10 +83,12 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myOtherApp: { read: true, all: true }, myAppWithSubFeature: { read: true, all: true }, }, + enabledInLicense: true, }, { actionGroups: [], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', @@ -92,10 +99,12 @@ describe('asFiltersByAlertTypeAndConsumer', () => { myOtherApp: { read: true, all: true }, myAppWithSubFeature: { read: true, all: true }, }, + enabledInLicense: true, }, { actionGroups: [], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', @@ -106,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..94db4c946ab00 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 test is disabled because it requires a Gold license. Contact your administrator to 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..dea5b3338a5be 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 {alertTypeId} is disabled because it requires a Gold license. Contact your administrator to upgrade your license.', + values: { alertTypeId: alertType.id }, + }), + 'license_invalid' + ); + default: + assertNever(check.reason); + } + } } export function verifyApiAccessFactory(licenseState: LicenseState) { diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts index 1e6c26c02e65b..2814eaef3e02a 100644 --- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts @@ -19,6 +19,7 @@ test('should return passed in params when validation not defined', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', }, @@ -41,6 +42,7 @@ test('should validate and apply defaults when params is valid', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', validate: { params: schema.object({ param1: schema.string(), @@ -71,6 +73,7 @@ test('should validate and throw error when params is invalid', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', validate: { params: schema.object({ param1: schema.string(), diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 48fd2e12336a8..0c9a09b11532b 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; +import { + AlertingPlugin, + AlertingPluginsSetup, + AlertingPluginsStart, + PluginSetupContract, +} from './plugin'; import { coreMock, statusServiceMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; @@ -14,9 +19,16 @@ import { KibanaRequest, CoreSetup } from 'kibana/server'; import { featuresPluginMock } from '../../features/server/mocks'; import { KibanaFeature } from '../../features/server'; import { AlertsConfig } from './config'; +import { AlertType } from './types'; +import { eventLogMock } from '../../event_log/server/mocks'; +import { actionsMock } from '../../actions/server/mocks'; describe('Alerting Plugin', () => { describe('setup()', () => { + let plugin: AlertingPlugin; + let coreSetup: ReturnType; + let pluginsSetup: jest.Mocked; + it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { const context = coreMock.createPluginInitializerContext({ healthCheck: { @@ -27,9 +39,9 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, }); - const plugin = new AlertingPlugin(context); + plugin = new AlertingPlugin(context); - const coreSetup = coreMock.createSetup(); + coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); const statusMock = statusServiceMock.createSetupContract(); await plugin.setup( @@ -55,6 +67,56 @@ describe('Alerting Plugin', () => { 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); }); + + describe('registerType()', () => { + let setup: PluginSetupContract; + const sampleAlertType: AlertType = { + id: 'test', + name: 'test', + minimumLicenseRequired: 'basic', + actionGroups: [], + defaultActionGroupId: 'default', + producer: 'test', + async executor() {}, + }; + + beforeEach(async () => { + coreSetup = coreMock.createSetup(); + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + licensing: licensingMock.createSetup(), + eventLog: eventLogMock.createSetup(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + }; + setup = await plugin.setup(coreSetup, pluginsSetup); + }); + + it('should throw error when license type is invalid', async () => { + expect(() => + setup.registerType({ + ...sampleAlertType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + minimumLicenseRequired: 'foo' as any, + }) + ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); + }); + + it('should not throw when license type is gold', async () => { + setup.registerType({ + ...sampleAlertType, + minimumLicenseRequired: 'gold', + }); + }); + + it('should not throw when license type is basic', async () => { + setup.registerType({ + ...sampleAlertType, + minimumLicenseRequired: 'basic', + }); + }); + }); }); describe('start()', () => { @@ -106,6 +168,7 @@ describe('Alerting Plugin', () => { }, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), features: mockFeatures(), + licensing: licensingMock.createStart(), } as unknown) as AlertingPluginsStart ); @@ -160,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 e526c65b90102..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,12 +54,20 @@ import { unmuteAlertInstanceRoute, healthRoute, } from './routes'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LICENSE_TYPE, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as ActionsPluginSetupContract, PluginStartContract as ActionsPluginStartContract, } from '../../actions/server'; -import { AlertsHealth, Services } from './types'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertsHealth, + AlertType, + AlertTypeParams, + AlertTypeState, + Services, +} from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server'; @@ -90,8 +98,16 @@ export const LEGACY_EVENT_LOG_ACTIONS = { }; export interface PluginSetupContract { - registerType: AlertTypeRegistry['register']; + registerType< + Params extends AlertTypeParams = AlertTypeParams, + State extends AlertTypeState = AlertTypeState, + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext + >( + alertType: AlertType + ): void; } + export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; @@ -114,6 +130,7 @@ export interface AlertingPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; features: FeaturesPluginStart; eventLog: IEventLogClientService; + licensing: LicensingPluginStart; spaces?: SpacesPluginStart; security?: SecurityPluginStart; } @@ -123,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; @@ -181,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; @@ -250,7 +269,17 @@ export class AlertingPlugin { healthRoute(router, this.licenseState, plugins.encryptedSavedObjects); return { - registerType: alertTypeRegistry.register.bind(alertTypeRegistry), + registerType< + Params extends AlertTypeParams = AlertTypeParams, + State extends AlertTypeState = AlertTypeState, + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext + >(alertType: AlertType) { + if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { + throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); + } + alertTypeRegistry.register(alertType); + }, }; } @@ -262,8 +291,11 @@ export class AlertingPlugin { alertTypeRegistry, alertsClientFactory, security, + licenseState, } = this; + licenseState?.setNotifyUsage(plugins.licensing.featureUsage.notifyUsage); + const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes: ['alert'], }); @@ -313,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 90c075f129b8c..5597b315158cd 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(); @@ -74,7 +75,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); @@ -134,7 +135,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); @@ -151,7 +152,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(() => { @@ -170,4 +171,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 f54aec8fe0cf0..a34a3118985fa 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, AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../types'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; export const bodySchema = schema.object({ name: schema.string(), @@ -41,7 +42,7 @@ export const bodySchema = schema.object({ notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })), }); -export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const createAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `${BASE_ALERT_API_PATH}/alert`, @@ -63,10 +64,17 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => const alertsClient = context.alerting.getAlertsClient(); const alert = req.body; const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as AlertNotifyWhenType) : null; - const alertRes: Alert = await alertsClient.create({ data: { ...alert, notifyWhen } }); - return res.ok({ - body: alertRes, - }); + try { + const alertRes: Alert = await alertsClient.create({ data: { ...alert, notifyWhen } }); + 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 51ac64bbef182..21e52ece82d2d 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'; @@ -60,7 +60,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); @@ -88,7 +88,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); @@ -111,7 +111,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 b18c79fd67484..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); @@ -44,6 +45,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, authorizedConsumers: {}, actionVariables: { @@ -51,7 +53,8 @@ describe('listAlertTypesRoute', () => { state: [], }, producer: 'test', - }, + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, ]; alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); @@ -73,7 +76,9 @@ describe('listAlertTypesRoute', () => { }, "authorizedConsumers": Object {}, "defaultActionGroupId": "default", + "enabledInLicense": true, "id": "1", + "minimumLicenseRequired": "basic", "name": "name", "producer": "test", "recoveryActionGroup": Object { @@ -93,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); @@ -113,6 +118,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, authorizedConsumers: {}, actionVariables: { @@ -120,7 +126,8 @@ describe('listAlertTypesRoute', () => { state: [], }, producer: 'alerts', - }, + enabledInLicense: true, + } as RegistryAlertTypeWithAuth, ]; alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); @@ -139,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(() => { @@ -163,6 +170,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, authorizedConsumers: {}, actionVariables: { @@ -170,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 89619bd853707..96c84616cba70 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'; import { AlertNotifyWhenType } from '../../common'; const alertsClient = alertsClientMock.create(); @@ -46,7 +47,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); @@ -124,7 +125,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); @@ -167,7 +168,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(() => { @@ -212,4 +213,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 96b3156525f79..d3ecc9eb3e381 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 { AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../../common'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; const paramSchema = schema.object({ id: schema.string(), @@ -42,7 +43,7 @@ const bodySchema = schema.object({ notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })), }); -export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => { +export const updateAlertRoute = (router: IRouter, licenseState: ILicenseState) => { router.put( { path: `${BASE_ALERT_API_PATH}/alert/{id}`, @@ -64,8 +65,8 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; const { name, actions, params, schedule, tags, throttle, notifyWhen } = req.body; - return res.ok({ - body: await alertsClient.update({ + try { + const alertRes = await alertsClient.update({ id, data: { name, @@ -76,8 +77,16 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => throttle, notifyWhen: notifyWhen as AlertNotifyWhenType, }, - }), - }); + }); + 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/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 67add495674da..4cb82e9cc86a1 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -24,6 +24,7 @@ const alertType: AlertType = { { id: 'other-group', name: 'Other Group' }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered', 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 5674687467fe2..7545f9a18c4ce 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 @@ -28,15 +28,19 @@ import { IEventLogger } from '../../../event_log/server'; 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', actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), producer: 'alerts', }; + let fakeTimer: sinon.SinonFakeTimers; describe('Task Runner', () => { @@ -69,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; @@ -83,6 +88,7 @@ describe('Task Runner', () => { basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), + alertTypeRegistry, }; const mockedAlertTypeSavedObject: Alert = { @@ -144,7 +150,7 @@ describe('Task Runner', () => { test('successfully executes the task', async () => { const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -240,7 +246,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -391,7 +397,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -501,7 +507,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -546,7 +552,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -640,7 +646,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -682,7 +688,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -728,7 +734,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -896,7 +902,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -996,7 +1002,7 @@ describe('Task Runner', () => { } ); const taskRunner = new TaskRunner( - alertTypeWithCustomRecovery, + alertTypeWithCustomRecovery as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -1088,7 +1094,7 @@ describe('Task Runner', () => { ); const date = new Date().toISOString(); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: { @@ -1218,7 +1224,7 @@ describe('Task Runner', () => { param1: schema.string(), }), }, - }, + } as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1246,7 +1252,7 @@ describe('Task Runner', () => { test('uses API key when provided', async () => { const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1279,7 +1285,7 @@ describe('Task Runner', () => { test(`doesn't use API key when not provided`, async () => { const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1309,7 +1315,7 @@ describe('Task Runner', () => { test('rescheduled the Alert if the schedule has update during a task run', async () => { const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1350,7 +1356,7 @@ describe('Task Runner', () => { ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1417,7 +1423,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1470,13 +1476,80 @@ 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'); }); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1543,7 +1616,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1613,7 +1686,7 @@ describe('Task Runner', () => { const legacyTaskInstance = omit(mockedTaskInstance, 'schedule'); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, legacyTaskInstance, taskRunnerFactoryInitializerParams ); @@ -1651,7 +1724,7 @@ describe('Task Runner', () => { ); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, { ...mockedTaskInstance, state: originalAlertSate, @@ -1682,7 +1755,7 @@ describe('Task Runner', () => { }); const taskRunner = new TaskRunner( - alertType, + alertType as NormalizedAlertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); 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 2073528f2c75e..17ab090610745 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) { @@ -365,6 +368,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 4c685d2fdec82..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 @@ -16,12 +16,15 @@ import { 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 = { +const alertType: NormalizedAlertType = { id: 'test', name: 'My test alert', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered', @@ -72,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/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index a5aee8dbf3b60..8704068c3e51a 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -30,6 +30,7 @@ import { AlertsHealth, AlertNotifyWhenType, } from '../common'; +import { LicenseType } from '../../licensing/server'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; @@ -84,6 +85,16 @@ export interface ActionVariable { description: string; } +// signature of the alert type executor function +export type ExecutorType< + Params, + State, + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +> = ( + options: AlertExecutorOptions +) => Promise; + export interface AlertType< Params extends AlertTypeParams = AlertTypeParams, State extends AlertTypeState = AlertTypeState, @@ -98,17 +109,14 @@ export interface AlertType< actionGroups: ActionGroup[]; defaultActionGroupId: ActionGroup['id']; recoveryActionGroup?: ActionGroup; - executor: ({ - services, - params, - state, - }: AlertExecutorOptions) => Promise; + executor: ExecutorType; producer: string; actionVariables?: { context?: ActionVariable[]; state?: ActionVariable[]; params?: ActionVariable[]; }; + minimumLicenseRequired: LicenseType; } export interface RawAlertAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index a234226d18034..7cc36253ef581 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -29,6 +29,7 @@ export const ALERT_TYPES_CONFIG = { }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', + minimumLicenseRequired: 'basic', producer: 'apm', }, [AlertType.TransactionDuration]: { @@ -37,6 +38,7 @@ export const ALERT_TYPES_CONFIG = { }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', + minimumLicenseRequired: 'basic', producer: 'apm', }, [AlertType.TransactionDurationAnomaly]: { @@ -45,6 +47,7 @@ export const ALERT_TYPES_CONFIG = { }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', + minimumLicenseRequired: 'basic', producer: 'apm', }, [AlertType.TransactionErrorRate]: { @@ -53,6 +56,7 @@ export const ALERT_TYPES_CONFIG = { }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', + minimumLicenseRequired: 'basic', producer: 'apm', }, }; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 124f61ed031fe..36fdf45d805f1 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -59,6 +59,7 @@ export function registerErrorCountAlertType({ ], }, producer: 'apm', + minimumLicenseRequired: 'basic', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); const alertParams = params; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index cad5f5f8b9b56..48fc0899f029c 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -68,6 +68,7 @@ export function registerTransactionDurationAlertType({ ], }, producer: 'apm', + minimumLicenseRequired: 'basic', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); const alertParams = params; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index b9e970ace869d..f3df0092dbbbd 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -67,6 +67,7 @@ export function registerTransactionDurationAnomalyAlertType({ ], }, producer: 'apm', + minimumLicenseRequired: 'basic', executor: async ({ services, params, state }) => { if (!ml) { return; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 2753b378754f8..766705e2803b1 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -65,6 +65,7 @@ export function registerTransactionErrorRateAlertType({ ], }, producer: 'apm', + minimumLicenseRequired: 'basic', executor: async ({ services, params: alertParams }) => { const config = await config$.pipe(take(1)).toPromise(); const indices = await getApmIndices({ diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 14d1acf0e4a9f..6ec6210ecb344 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; +import { AlertType } from '../../../../../alerts/server'; import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, @@ -38,7 +39,7 @@ const condition = schema.object({ ), }); -export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ +export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs): AlertType => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: 'Inventory', validate: { @@ -58,6 +59,7 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], producer: 'infrastructure', + minimumLicenseRequired: 'basic', executor: createInventoryMetricThresholdExecutor(libs), actionVariables: { context: [ diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index 34afddc2a4d48..64bfad92a8458 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -89,6 +89,7 @@ export async function registerLogThresholdAlertType( }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], + minimumLicenseRequired: 'basic', executor: createLogThresholdExecutor(libs), actionVariables: { context: [ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 45b1df2f03ea1..1a10765eaf734 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; +import { AlertType } from '../../../../../alerts/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; @@ -19,7 +20,7 @@ import { thresholdActionVariableDescription, } from '../common/messages'; -export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { +export function registerMetricThresholdAlertType(libs: InfraBackendLibs): AlertType { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), @@ -60,6 +61,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], + minimumLicenseRequired: 'basic', executor: createMetricThresholdExecutor(libs), actionVariables: { context: [ diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 4f989b37421ef..ebff72a255777 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -107,6 +107,7 @@ export class BaseAlert { }, ], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', executor: (options: AlertExecutorOptions & { state: ExecutedState }): Promise => this.execute(options), producer: 'monitoring', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index d60e5dbd5ab4c..6bda5a599c83d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -35,6 +35,7 @@ export const rulesNotificationAlertType = ({ ruleAlertId: schema.string(), }), }, + minimumLicenseRequired: 'basic', async executor({ startedAt, previousStartedAt, alertId, services, params }) { const ruleAlertSavedObject = await services.savedObjectsClient.get( 'alert', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index fdd1fed2e6e89..7fd99a17598ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -91,6 +91,7 @@ export const signalRulesAlertType = ({ params: signalParamsSchema(), }, producer: SERVER_APP_ID, + minimumLicenseRequired: 'basic', async executor({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index a873cab69f23b..164ce993eebac 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -9,12 +9,7 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoContainmentExecutor } from './geo_containment'; -import { - ActionGroup, - AlertServices, - ActionVariable, - AlertTypeState, -} from '../../../../alerts/server'; +import { AlertType } from '../../../../alerts/server'; import { Query } from '../../../../../../src/plugins/data/common/query'; export const GEO_CONTAINMENT_ID = '.geo-containment'; @@ -117,40 +112,7 @@ export interface GeoContainmentParams { boundaryIndexQuery?: Query; } -export function getAlertType( - logger: Logger -): { - defaultActionGroupId: string; - actionGroups: ActionGroup[]; - executor: ({ - previousStartedAt: currIntervalStartTime, - startedAt: currIntervalEndTime, - services, - params, - alertId, - state, - }: { - previousStartedAt: Date | null; - startedAt: Date; - services: AlertServices; - params: GeoContainmentParams; - alertId: string; - state: AlertTypeState; - }) => Promise; - validate?: { - params?: { - validate: (object: unknown) => GeoContainmentParams; - }; - }; - name: string; - producer: string; - id: string; - actionVariables?: { - context?: ActionVariable[]; - state?: ActionVariable[]; - params?: ActionVariable[]; - }; -} { +export function getAlertType(logger: Logger): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoContainment.alertTypeTitle', { defaultMessage: 'Geo tracking containment', }); @@ -173,5 +135,6 @@ export function getAlertType( params: ParamsSchema, }, actionVariables, + minimumLicenseRequired: 'gold', }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts index 2fa2bed9d8419..02116d0701bfa 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts @@ -6,7 +6,7 @@ import { Logger } from 'src/core/server'; import { AlertingSetup } from '../../types'; -import { getAlertType } from './alert_type'; +import { GeoContainmentParams, getAlertType } from './alert_type'; interface RegisterParams { logger: Logger; @@ -15,5 +15,5 @@ interface RegisterParams { export function register(params: RegisterParams) { const { logger, alerts } = params; - alerts.registerType(getAlertType(logger)); + alerts.registerType(getAlertType(logger)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 0c40f5b5f3866..93a6c0d29cf3c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -9,12 +9,7 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoThresholdExecutor } from './geo_threshold'; -import { - ActionGroup, - AlertServices, - ActionVariable, - AlertTypeState, -} from '../../../../alerts/server'; +import { AlertType } from '../../../../alerts/server'; import { Query } from '../../../../../../src/plugins/data/common/query'; export const GEO_THRESHOLD_ID = '.geo-threshold'; @@ -177,40 +172,7 @@ export interface GeoThresholdParams { boundaryIndexQuery?: Query; } -export function getAlertType( - logger: Logger -): { - defaultActionGroupId: string; - actionGroups: ActionGroup[]; - executor: ({ - previousStartedAt: currIntervalStartTime, - startedAt: currIntervalEndTime, - services, - params, - alertId, - state, - }: { - previousStartedAt: Date | null; - startedAt: Date; - services: AlertServices; - params: GeoThresholdParams; - alertId: string; - state: AlertTypeState; - }) => Promise; - validate?: { - params?: { - validate: (object: unknown) => GeoThresholdParams; - }; - }; - name: string; - producer: string; - id: string; - actionVariables?: { - context?: ActionVariable[]; - state?: ActionVariable[]; - params?: ActionVariable[]; - }; -} { +export function getAlertType(logger: Logger): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoThreshold.alertTypeTitle', { defaultMessage: 'Geo tracking threshold', }); @@ -233,5 +195,6 @@ export function getAlertType( params: ParamsSchema, }, actionVariables, + minimumLicenseRequired: 'gold', }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index 9de0940771525..9600395c78218 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -143,6 +143,7 @@ export function getAlertType( ...alertParamsVariables, ], }, + minimumLicenseRequired: 'basic', executor, producer: STACK_ALERTS_FEATURE_ID, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index daf51dcd43812..ef4abcc758e44 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -321,5 +321,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType { recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, authorizedConsumers: {}, producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 32b663c5693fc..538c6be89ab4b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -51,6 +51,8 @@ describe('loadAlertTypes', () => { recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, defaultActionGroupId: 'default', authorizedConsumers: {}, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts new file mode 100644 index 0000000000000..e364661361814 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.test.ts @@ -0,0 +1,182 @@ +/* + * 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 { AlertTypeModel } from '../../types'; +import { alertTypeGroupCompare, alertTypeCompare } from './alert_type_compare'; +import { IsEnabledResult, IsDisabledResult } from './check_alert_type_enabled'; + +test('should sort groups by containing enabled alert types first and then by name', async () => { + const alertTypes: Array< + [ + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }> + ] + > = [ + [ + 'abc', + [ + { + id: '1', + name: 'test2', + checkEnabledResult: { isEnabled: false, message: 'gold license' }, + alertTypeItem: { + id: 'my-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'bcd', + [ + { + id: '2', + name: 'abc', + checkEnabledResult: { isEnabled: false, message: 'platinum license' }, + alertTypeItem: { + id: 'my-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + { + id: '3', + name: 'cdf', + checkEnabledResult: { isEnabled: true }, + alertTypeItem: { + id: 'disabled-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'cde', + [ + { + id: '4', + name: 'cde', + checkEnabledResult: { isEnabled: true }, + alertTypeItem: { + id: 'my-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + ]; + + const groups = new Map(); + groups.set('abc', 'ABC'); + groups.set('bcd', 'BCD'); + groups.set('cde', 'CDE'); + + const result = [...alertTypes].sort((right, left) => alertTypeGroupCompare(right, left, groups)); + expect(result[0]).toEqual(alertTypes[1]); + expect(result[1]).toEqual(alertTypes[2]); + expect(result[2]).toEqual(alertTypes[0]); +}); + +test('should sort alert types by enabled first and then by name', async () => { + const alertTypes: Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }> = [ + { + id: '1', + name: 'bcd', + checkEnabledResult: { isEnabled: false, message: 'gold license' }, + alertTypeItem: { + id: 'my-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + { + id: '2', + name: 'abc', + checkEnabledResult: { isEnabled: false, message: 'platinum license' }, + alertTypeItem: { + id: 'my-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + { + id: '3', + name: 'cdf', + checkEnabledResult: { isEnabled: true }, + alertTypeItem: { + id: 'disabled-alert-type', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ]; + const result = [...alertTypes].sort(alertTypeCompare); + expect(result[0]).toEqual(alertTypes[2]); + expect(result[1]).toEqual(alertTypes[1]); + expect(result[2]).toEqual(alertTypes[0]); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts new file mode 100644 index 0000000000000..68df0220a4bec --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_type_compare.ts @@ -0,0 +1,77 @@ +/* + * 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 { AlertTypeModel } from '../../types'; +import { IsEnabledResult, IsDisabledResult } from './check_alert_type_enabled'; + +export function alertTypeGroupCompare( + left: [ + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }> + ], + right: [ + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }> + ], + groupNames: Map | undefined +) { + const groupNameA = left[0]; + const groupNameB = right[0]; + const leftAlertTypesList = left[1]; + const rightAlertTypesList = right[1]; + + const hasEnabledAlertTypeInListLeft = + leftAlertTypesList.find((alertTypeItem) => alertTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + const hasEnabledAlertTypeInListRight = + rightAlertTypesList.find((alertTypeItem) => alertTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + if (hasEnabledAlertTypeInListLeft && !hasEnabledAlertTypeInListRight) { + return -1; + } + if (!hasEnabledAlertTypeInListLeft && hasEnabledAlertTypeInListRight) { + return 1; + } + + return groupNames + ? groupNames.get(groupNameA)!.localeCompare(groupNames.get(groupNameB)!) + : groupNameA.localeCompare(groupNameB); +} + +export function alertTypeCompare( + a: { + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }, + b: { + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + } +) { + if (a.checkEnabledResult.isEnabled === true && b.checkEnabledResult.isEnabled === false) { + return -1; + } + if (a.checkEnabledResult.isEnabled === false && b.checkEnabledResult.isEnabled === true) { + return 1; + } + return a.name.localeCompare(b.name); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx new file mode 100644 index 0000000000000..fa70e2fae1384 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { AlertType } from '../../types'; +import { checkAlertTypeEnabled } from './check_alert_type_enabled'; + +describe('checkAlertTypeEnabled', () => { + test(`returns isEnabled:true when alert type isn't provided`, async () => { + expect(checkAlertTypeEnabled()).toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); + }); + + test('returns isEnabled:true when alert type is enabled', async () => { + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], + }, + producer: 'test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }; + expect(checkAlertTypeEnabled(alertType)).toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); + }); + + test('returns isEnabled:false when alert type is disabled by license', async () => { + const alertType: AlertType = { + id: 'test', + name: 'Test', + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], + }, + producer: 'test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + minimumLicenseRequired: 'gold', + enabledInLicense: false, + }; + expect(checkAlertTypeEnabled(alertType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This alert type requires a Gold license.", + } + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx new file mode 100644 index 0000000000000..a4d5c1e01da41 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_alert_type_enabled.tsx @@ -0,0 +1,40 @@ +/* + * 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 { upperFirst } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { AlertType } from '../../types'; + +export interface IsEnabledResult { + isEnabled: true; +} +export interface IsDisabledResult { + isEnabled: false; + message: string; +} + +const getLicenseCheckResult = (alertType: AlertType) => { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkAlertTypeEnabled.alertTypeDisabledByLicenseMessage', + { + defaultMessage: 'This alert type requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: upperFirst(alertType.minimumLicenseRequired), + }, + } + ), + }; +}; + +export function checkAlertTypeEnabled(alertType?: AlertType): IsEnabledResult | IsDisabledResult { + if (alertType?.enabledInLicense === false) { + return getLicenseCheckResult(alertType); + } + + return { isEnabled: true }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index c10653d14d409..e25e703de5f7e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; -import { Alert, ActionType, AlertTypeModel } from '../../../../types'; +import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types'; import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; import { ViewInApp } from './view_in_app'; import { @@ -54,15 +54,17 @@ describe('alert_details', () => { it('renders the alert name as a title', () => { const alert = mockAlert(); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup, actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', producer: ALERTS_FEATURE_ID, authorizedConsumers, + enabledInLicense: true, }; expect( @@ -80,15 +82,17 @@ describe('alert_details', () => { it('renders the alert type badge', () => { const alert = mockAlert(); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup, actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', producer: ALERTS_FEATURE_ID, authorizedConsumers, + enabledInLicense: true, }; expect( @@ -109,15 +113,17 @@ describe('alert_details', () => { }, }, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup, actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', producer: ALERTS_FEATURE_ID, authorizedConsumers, + enabledInLicense: true, }; expect( @@ -144,15 +150,17 @@ describe('alert_details', () => { ], }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], recoveryActionGroup, actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', producer: ALERTS_FEATURE_ID, authorizedConsumers, + enabledInLicense: true, }; const actionTypes: ActionType[] = [ @@ -199,7 +207,7 @@ describe('alert_details', () => { }, ], }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -207,7 +215,9 @@ describe('alert_details', () => { actionVariables: { context: [], state: [], params: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'basic', authorizedConsumers, + enabledInLicense: true, }; const actionTypes: ActionType[] = [ { @@ -259,7 +269,7 @@ describe('alert_details', () => { it('links to the app that created the alert', () => { const alert = mockAlert(); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -268,6 +278,8 @@ describe('alert_details', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; expect( @@ -280,7 +292,7 @@ describe('alert_details', () => { it('links to the Edit flyout', () => { const alert = mockAlert(); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -289,6 +301,8 @@ describe('alert_details', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; expect( @@ -310,7 +324,7 @@ describe('disable button', () => { enabled: true, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -319,6 +333,8 @@ describe('disable button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableButton = shallow( @@ -339,7 +355,7 @@ describe('disable button', () => { enabled: false, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -348,6 +364,8 @@ describe('disable button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableButton = shallow( @@ -368,7 +386,7 @@ describe('disable button', () => { enabled: true, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -377,6 +395,8 @@ describe('disable button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const disableAlert = jest.fn(); @@ -406,7 +426,7 @@ describe('disable button', () => { enabled: false, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -415,6 +435,8 @@ describe('disable button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableAlert = jest.fn(); @@ -447,7 +469,7 @@ describe('mute button', () => { muteAll: false, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -456,6 +478,8 @@ describe('mute button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableButton = shallow( @@ -477,7 +501,7 @@ describe('mute button', () => { muteAll: true, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -486,6 +510,8 @@ describe('mute button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableButton = shallow( @@ -507,7 +533,7 @@ describe('mute button', () => { muteAll: false, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -516,6 +542,8 @@ describe('mute button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const muteAlert = jest.fn(); @@ -546,7 +574,7 @@ describe('mute button', () => { muteAll: true, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -555,6 +583,8 @@ describe('mute button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const unmuteAlert = jest.fn(); @@ -585,7 +615,7 @@ describe('mute button', () => { muteAll: false, }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -594,6 +624,8 @@ describe('mute button', () => { defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; const enableButton = shallow( @@ -622,7 +654,7 @@ describe('edit button', () => { }, ]; alertTypeRegistry.has.mockReturnValue(true); - const alertTypeR = ({ + const alertTypeR: AlertTypeModel = { id: 'my-alert-type', iconClass: 'test', name: 'test-alert', @@ -631,9 +663,9 @@ describe('edit button', () => { validate: () => { return { errors: {} }; }, - alertParamsExpression: () => {}, + alertParamsExpression: jest.fn(), requiresAppContext: false, - } as unknown) as AlertTypeModel; + }; alertTypeRegistry.get.mockReturnValue(alertTypeR); useKibanaMock().services.alertTypeRegistry = alertTypeRegistry; @@ -651,7 +683,7 @@ describe('edit button', () => { ], }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -660,6 +692,8 @@ describe('edit button', () => { defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; expect( @@ -694,7 +728,7 @@ describe('edit button', () => { ], }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -703,6 +737,8 @@ describe('edit button', () => { defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; expect( @@ -730,7 +766,7 @@ describe('edit button', () => { actions: [], }); - const alertType = { + const alertType: AlertType = { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], @@ -739,6 +775,8 @@ describe('edit button', () => { defaultActionGroupId: 'default', producer: 'alerting', authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, }; expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0c1b00d78d198..5bb8d47988eed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -26,6 +26,7 @@ import { EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertExecutionStatusErrorReasons } from '../../../../../../alerts/common'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; @@ -66,6 +67,7 @@ export const AlertDetails: React.FunctionComponent = ({ actionTypeRegistry, setBreadcrumbs, chrome, + http, } = useKibana().services; const [{}, dispatch] = useReducer(alertReducer, { alert }); const setInitialAlert = (value: Alert) => { @@ -139,6 +141,7 @@ export const AlertDetails: React.FunctionComponent = ({ iconType="pencil" onClick={() => setEditFlyoutVisibility(true)} name="edit" + disabled={!alertType.enabledInLicense} > = ({ { @@ -235,7 +238,7 @@ export const AlertDetails: React.FunctionComponent = ({ { if (isMuted) { @@ -272,12 +275,31 @@ export const AlertDetails: React.FunctionComponent = ({ {alert.executionStatus.error?.message} - setDissmissAlertErrors(true)}> - - + + + setDissmissAlertErrors(true)}> + + + + {alert.executionStatus.error?.reason === + AlertExecutionStatusErrorReasons.License && ( + + + + + + )} + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx index be68036c0f743..d65f1d3af1754 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx @@ -311,6 +311,8 @@ function mockAlertType(overloads: Partial = {}): AlertType { recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, authorizedConsumers: {}, producer: 'alerts', + minimumLicenseRequired: 'basic', + enabledInLicense: true, ...overloads, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx index b24d552bd5c48..e3fe9cd86356a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx @@ -151,6 +151,8 @@ function mockAlertType(overloads: Partial = {}): AlertType { recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, authorizedConsumers: {}, producer: 'alerts', + minimumLicenseRequired: 'basic', + enabledInLicense: true, ...overloads, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 598260512d3bf..2790ea8aa6bfa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -56,6 +56,7 @@ describe('alert_add', () => { }, ], defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, producer: ALERTS_FEATURE_ID, authorizedConsumers: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 26aca1bb5e4a0..d41ca915f34c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -9,7 +9,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { ValidationResult, Alert } from '../../../types'; +import { ValidationResult, Alert, AlertType } from '../../../types'; import { AlertForm } from './alert_form'; import { coreMock } from 'src/core/public/mocks'; import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '../../../../../alerts/common'; @@ -63,6 +63,20 @@ describe('alert_form', () => { alertParamsExpression: () => , requiresAppContext: true, }; + + const disabledByLicenseAlertType = { + id: 'disabled-by-license', + iconClass: 'test', + name: 'test-alert', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: (): ValidationResult => { + return { errors: {} }; + }, + alertParamsExpression: () => , + requiresAppContext: false, + }; + const useKibanaMock = useKibana as jest.Mocked; describe('alert_form create alert', () => { @@ -71,7 +85,7 @@ describe('alert_form', () => { async function setup() { const mocks = coreMock.createSetup(); const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); - const alertTypes = [ + const alertTypes: AlertType[] = [ { id: 'my-alert-type', name: 'Test', @@ -82,12 +96,41 @@ describe('alert_form', () => { }, ], defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: ALERTS_FEATURE_ID, authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, test: { read: true, all: true }, }, + actionVariables: { + params: [], + state: [], + }, + enabledInLicense: true, + }, + { + id: 'disabled-by-license', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'gold', + recoveryActionGroup: RecoveredActionGroup, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, + actionVariables: { + params: [], + state: [], + }, + enabledInLicense: false, }, ]; loadAlertTypes.mockResolvedValue(alertTypes); @@ -105,7 +148,11 @@ describe('alert_form', () => { delete: true, }, }; - alertTypeRegistry.list.mockReturnValue([alertType, alertTypeNonEditable]); + alertTypeRegistry.list.mockReturnValue([ + alertType, + alertTypeNonEditable, + disabledByLicenseAlertType, + ]); alertTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.list.mockReturnValue([actionType]); actionTypeRegistry.has.mockReturnValue(true); @@ -185,6 +232,15 @@ describe('alert_form', () => { expect(alertDocumentationLink.exists()).toBeTruthy(); expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); }); + + it('renders alert types disabled by license', async () => { + await setup(); + const actionOption = wrapper.find(`[data-test-subj="disabled-by-license-SelectOption"]`); + expect(actionOption.exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="disabled-by-license-disabledTooltip"]').exists() + ).toBeTruthy(); + }); }); describe('alert_form create alert non alerting consumer and producer', () => { @@ -204,6 +260,7 @@ describe('alert_form', () => { }, ], defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: ALERTS_FEATURE_ID, authorizedConsumers: { @@ -221,6 +278,7 @@ describe('alert_form', () => { }, ], defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', recoveryActionGroup: RecoveredActionGroup, producer: 'test', authorizedConsumers: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 6ea16bf1f226a..3210d53841993 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -31,6 +31,7 @@ import { EuiText, EuiNotificationBadge, EuiErrorBoundary, + EuiToolTip, } from '@elastic/eui'; import { capitalize, isObject } from 'lodash'; import { KibanaFeature } from '../../../../../features/public'; @@ -65,7 +66,11 @@ import './alert_form.scss'; import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage } from '../../constants'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; +import { IsEnabledResult, IsDisabledResult } from '../../lib/check_alert_type_enabled'; import { AlertNotifyWhen } from './alert_notify_when'; +import { checkAlertTypeEnabled } from '../../lib/check_alert_type_enabled'; +import { alertTypeCompare, alertTypeGroupCompare } from '../../lib/alert_type_compare'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; const ENTER_KEY = 13; @@ -182,6 +187,7 @@ export const AlertForm = ({ const [inputText, setInputText] = useState(); const [solutions, setSolutions] = useState | undefined>(undefined); const [solutionsFilter, setSolutionFilter] = useState([]); + let hasDisabledByLicenseAlertTypes: boolean = false; // load alert types useEffect(() => { @@ -354,17 +360,30 @@ export const AlertForm = ({ const alertTypesByProducer = filteredAlertTypes.reduce( ( - result: Record>, + result: Record< + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + alertTypeItem: AlertTypeModel; + }> + >, alertTypeValue ) => { const producer = alertTypeValue.alertType.producer; if (producer) { + const checkEnabledResult = checkAlertTypeEnabled(alertTypeValue.alertType); + if (!checkEnabledResult.isEnabled) { + hasDisabledByLicenseAlertTypes = true; + } (result[producer] = result[producer] || []).push({ name: typeof alertTypeValue.alertTypeModel.name === 'string' ? alertTypeValue.alertTypeModel.name : alertTypeValue.alertTypeModel.name.props.defaultMessage, id: alertTypeValue.alertTypeModel.id, + checkEnabledResult, alertTypeItem: alertTypeValue.alertTypeModel, }); } @@ -374,9 +393,7 @@ export const AlertForm = ({ ); const alertTypeNodes = Object.entries(alertTypesByProducer) - .sort(([a], [b]) => - solutions ? solutions.get(a)!.localeCompare(solutions.get(b)!) : a.localeCompare(b) - ) + .sort((a, b) => alertTypeGroupCompare(a, b, solutions)) .map(([solution, items], groupIndex) => ( {items - .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) - .map((item, index) => ( - - - {item.name} - -

{item.alertTypeItem.description}

-
- - } - onClick={() => { - setAlertProperty('alertTypeId', item.id); - setAlertTypeModel(item.alertTypeItem); - setAlertProperty('params', {}); - setActions([]); - if (alertTypesIndex && alertTypesIndex.has(item.id)) { - setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); + .sort((a, b) => alertTypeCompare(a, b)) + .map((item, index) => { + const alertTypeListItemHtml = ( + + {item.name} + +

{item.alertTypeItem.description}

+
+
+ ); + return ( + + + {alertTypeListItemHtml} + + ) } - }} - /> - - ))} + isDisabled={!item.checkEnabledResult.isEnabled} + onClick={() => { + setAlertProperty('alertTypeId', item.id); + setActions([]); + setAlertTypeModel(item.alertTypeItem); + setAlertProperty('params', {}); + if (alertTypesIndex && alertTypesIndex.has(item.id)) { + setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); + } + }} + /> +
+ ); + })}
@@ -710,6 +743,23 @@ export const AlertForm = ({ + + + + + ) + } label={
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss new file mode 100644 index 0000000000000..fda7d6aa0b622 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss @@ -0,0 +1,7 @@ +.actAlertsList__tableRowDisabled { + background-color: $euiColorLightestShade; + + .actAlertsList__tableCellDisabled { + color: $euiColorDarkShade; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index f35c10b228369..7df5c6e157106 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -61,6 +61,7 @@ const alertTypeFromApi = { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'basic', authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index e1fadeade102d..1369e6e8f3b82 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -26,6 +26,7 @@ import { EuiButtonEmpty, EuiHealth, EuiText, + EuiToolTip, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -57,6 +58,8 @@ import { import { hasAllPrivilege } from '../../../lib/capabilities'; import { alertsStatusesTranslationsMapping } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; +import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; +import './alerts_list.scss'; const ENTER_KEY = 13; @@ -255,7 +258,10 @@ export const AlertsList: React.FunctionComponent = () => { truncateText: true, 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { - return ( + const checkEnabledResult = checkAlertTypeEnabled( + alertTypesState.data.get(alert.alertTypeId) + ); + const link = ( { @@ -265,6 +271,17 @@ export const AlertsList: React.FunctionComponent = () => { {name} ); + return checkEnabledResult.isEnabled ? ( + link + ) : ( + + {link} + + ); }, }, { @@ -572,11 +589,17 @@ export const AlertsList: React.FunctionComponent = () => { } itemId="id" columns={alertsTableColumns} - rowProps={() => ({ + rowProps={(item: AlertTableItem) => ({ 'data-test-subj': 'alert-row', + className: !alertTypesState.data.get(item.alertTypeId)?.enabledInLicense + ? 'actAlertsList__tableRowDisabled' + : '', })} - cellProps={() => ({ + cellProps={(item: AlertTableItem) => ({ 'data-test-subj': 'cell', + className: !alertTypesState.data.get(item.alertTypeId)?.enabledInLicense + ? 'actAlertsList__tableCellDisabled' + : '', })} data-test-subj="alertsList" pagination={{ @@ -707,5 +730,6 @@ function convertAlertsToTableItems( isEditable: hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)) && (canExecuteActions || (!canExecuteActions && !alert.actions.length)), + enabledInLicense: !!alertTypesIndex.get(alert.alertTypeId)?.enabledInLicense, })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index 9279f8a1745fc..0817cb34dc6f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -68,7 +68,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
= ({ { 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..a57cca5476420 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.alertErrorReasonLicense', + { + defaultMessage: 'Cannot run 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/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx index f87768c8d4537..2a58e51c2abd7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { Alert } from '../../../../types'; +import { AlertTableItem } from '../../../../types'; import { withBulkAlertOperations, ComponentOpts as BulkOperationsComponentOpts, @@ -18,7 +18,7 @@ import './alert_quick_edit_buttons.scss'; import { useKibana } from '../../../../common/lib/kibana'; export type ComponentOpts = { - selectedItems: Alert[]; + selectedItems: AlertTableItem[]; onPerformingAction?: () => void; onActionPerformed?: () => void; setAlertsToDelete: React.Dispatch>; @@ -49,6 +49,10 @@ export const AlertQuickEditButtons: React.FunctionComponent = ({ const isPerformingAction = isMutingAlerts || isUnmutingAlerts || isEnablingAlerts || isDisablingAlerts || isDeletingAlerts; + const hasDisabledByLicenseAlertTypes = !!selectedItems.find( + (alertItem) => !alertItem.enabledInLicense + ); + async function onmMuteAllClick() { onPerformingAction(); setIsMutingAlerts(true); @@ -156,7 +160,7 @@ export const AlertQuickEditButtons: React.FunctionComponent = ({ = ({ = ({ = ({ = ({ export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); -function isAlertDisabled(alert: Alert) { +function isAlertDisabled(alert: AlertTableItem) { return alert.enabled === false; } -function isAlertMuted(alert: Alert) { +function isAlertMuted(alert: AlertTableItem) { return alert.muteAll === true; } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index f950bbbd8ed25..cd1ebe47a8c22 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -11,6 +11,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ActionGroup, AlertActionParam } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; +import { AlertType as CommonAlertType } from '../../alerts/common'; import { SanitizedAlert as Alert, AlertAction, @@ -140,15 +141,20 @@ export const OPTIONAL_ACTION_VARIABLES = ['context'] as const; export type ActionVariables = AsActionVariables & Partial>; -export interface AlertType { - id: string; - name: string; - actionGroups: ActionGroup[]; - recoveryActionGroup: ActionGroup; +export interface AlertType + extends Pick< + CommonAlertType, + | 'id' + | 'name' + | 'actionGroups' + | 'producer' + | 'minimumLicenseRequired' + | 'recoveryActionGroup' + | 'defaultActionGroupId' + > { actionVariables: ActionVariables; - defaultActionGroupId: ActionGroup['id']; authorizedConsumers: Record; - producer: string; + enabledInLicense: boolean; } export type SanitizedAlertType = Omit; @@ -159,6 +165,7 @@ export interface AlertTableItem extends Alert { alertType: AlertType['name']; tagsText: string; isEditable: boolean; + enabledInLicense: boolean; } export interface AlertTypeParamsExpressionProps< diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 022ec48bad1d9..f5e79ad43336b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -82,6 +82,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _li context: [], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, + minimumLicenseRequired: 'basic', async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) { const { services: { alertInstanceFactory }, diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 3e45ce302bf87..56ca7a85784c5 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -255,6 +255,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = ], state: [...commonMonitorStateI18, ...commonStateTranslations], }, + minimumLicenseRequired: 'basic', async executor({ options: { params: rawParams, diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 41a5101716122..b6501f7d92059 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -100,6 +100,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => context: [], state: [...tlsTranslations.actionVariables, ...commonStateTranslations], }, + minimumLicenseRequired: 'basic', async executor({ options, dynamicSettings, uptimeEsClient }) { const { services: { alertInstanceFactory }, 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..3ba9d43cdedf0 --- /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 test.gold.noop is disabled because it requires a Gold license. Contact your administrator to 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 a3ff4d91e0d43..11065edd4beeb 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 @@ -6,7 +6,7 @@ import { CoreSetup } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; -import { times } from 'lodash'; +import { curry, times } from 'lodash'; import { ES_TEST_INDEX_NAME } from '../../../../lib'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; import { @@ -52,72 +52,73 @@ function getAlwaysFiringAlertType() { }, producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', actionVariables: { state: [{ name: 'instanceStateValue', description: 'the instance state value' }], params: [{ name: 'instanceParamsValue', description: 'the instance params value' }], context: [{ name: 'instanceContextValue', description: 'the instance context value' }], }, - async executor(alertExecutorOptions) { - const { - services, - params, - state, - alertId, - spaceId, - namespace, - name, - tags, - createdBy, - updatedBy, - } = alertExecutorOptions; - let group: string | null = 'default'; - let subgroup: string | null = null; - const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy }; + executor: curry(alwaysFiringExecutor)(), + }; + return result; +} - if (params.groupsToScheduleActionsInSeries) { - const index = state.groupInSeriesIndex || 0; - const [scheduledGroup, scheduledSubgroup] = ( - params.groupsToScheduleActionsInSeries[index] ?? '' - ).split(':'); +async function alwaysFiringExecutor(alertExecutorOptions: any) { + const { + services, + params, + state, + alertId, + spaceId, + namespace, + name, + tags, + createdBy, + updatedBy, + } = alertExecutorOptions; + let group: string | null = 'default'; + let subgroup: string | null = null; + const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy }; - group = scheduledGroup; - subgroup = scheduledSubgroup; - } + if (params.groupsToScheduleActionsInSeries) { + const index = state.groupInSeriesIndex || 0; + const [scheduledGroup, scheduledSubgroup] = ( + params.groupsToScheduleActionsInSeries[index] ?? '' + ).split(':'); - if (group) { - const instance = services - .alertInstanceFactory('1') - .replaceState({ instanceStateValue: true }); + group = scheduledGroup; + subgroup = scheduledSubgroup; + } - if (subgroup) { - instance.scheduleActionsWithSubGroup(group, subgroup, { - instanceContextValue: true, - }); - } else { - instance.scheduleActions(group, { - instanceContextValue: true, - }); - } - } + if (group) { + const instance = services.alertInstanceFactory('1').replaceState({ instanceStateValue: true }); - await services.scopedClusterClient.index({ - index: params.index, - refresh: 'wait_for', - body: { - state, - params, - reference: params.reference, - source: 'alert:test.always-firing', - alertInfo, - }, + if (subgroup) { + instance.scheduleActionsWithSubGroup(group, subgroup, { + instanceContextValue: true, }); - return { - globalStateValue: true, - groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, - }; + } else { + instance.scheduleActions(group, { + instanceContextValue: true, + }); + } + } + + await services.scopedClusterClient.index({ + index: params.index, + refresh: 'wait_for', + body: { + state, + params, + reference: params.reference, + source: 'alert:test.always-firing', + alertInfo, }, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, }; - return result; } function getCumulativeFiringAlertType() { @@ -136,6 +137,7 @@ function getCumulativeFiringAlertType() { ], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor(alertExecutorOptions) { const { services, state } = alertExecutorOptions; const group = 'default'; @@ -154,7 +156,7 @@ function getCumulativeFiringAlertType() { }; }, }; - return result; + return result as AlertType; } function getNeverFiringAlertType() { @@ -180,6 +182,7 @@ function getNeverFiringAlertType() { }, producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor({ services, params, state }) { await services.callCluster('index', { index: params.index, @@ -219,6 +222,7 @@ function getFailingAlertType() { ], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor({ services, params, state }) { await services.callCluster('index', { index: params.index, @@ -257,6 +261,7 @@ function getAuthorizationAlertType(core: CoreSetup) { ], defaultActionGroupId: 'default', producer: 'alertsFixture', + minimumLicenseRequired: 'basic', validate: { params: paramsSchema, }, @@ -342,6 +347,7 @@ function getValidationAlertType() { }, ], producer: 'alertsFixture', + minimumLicenseRequired: 'basic', defaultActionGroupId: 'default', validate: { params: paramsSchema, @@ -369,6 +375,7 @@ function getPatternFiringAlertType() { actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor(alertExecutorOptions) { const { services, state, params } = alertExecutorOptions; const pattern = params.pattern; @@ -429,6 +436,16 @@ export function defineAlertTypes( actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsFixture', defaultActionGroupId: 'default', + 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 = { @@ -437,6 +454,7 @@ export function defineAlertTypes( actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', actionVariables: { context: [{ name: 'aContextVariable', description: 'this is a context variable' }], }, @@ -451,6 +469,7 @@ export function defineAlertTypes( actionVariables: { state: [{ name: 'aStateVariable', description: 'this is a state variable' }], }, + minimumLicenseRequired: 'basic', async executor() {}, }; const throwAlertType: AlertType = { @@ -464,6 +483,7 @@ export function defineAlertTypes( ], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() { throw new Error('this alert is intended to fail'); }, @@ -479,6 +499,7 @@ export function defineAlertTypes( ], producer: 'alertsFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() { await new Promise((resolve) => setTimeout(resolve, 5000)); }, @@ -496,4 +517,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/common/fixtures/plugins/alerts_restricted/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts index 3e3c44f2c2784..3a81d41a2ca9c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts @@ -18,6 +18,7 @@ export function defineAlertTypes( actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsRestrictedFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'restrictedRecovered', name: 'Restricted Recovery' }, async executor({ services, params, state }: AlertExecutorOptions) {}, }; @@ -27,6 +28,7 @@ export function defineAlertTypes( actionGroups: [{ id: 'default', name: 'Default' }], producer: 'alertsRestrictedFixture', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor({ services, params, state }: AlertExecutorOptions) {}, }; alerts.registerType(noopRestrictedAlertType); 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 1ce04683f79bf..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 @@ -28,10 +28,12 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { params: [], }, producer: 'alertsFixture', + minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered', }, + enabledInLicense: true, }; const expectedRestrictedNoOpType = { @@ -52,6 +54,8 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { params: [], }, 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 c76a43b05b172..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 @@ -40,6 +40,8 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { name: 'Recovered', }, producer: 'alertsFixture', + minimumLicenseRequired: 'basic', + enabledInLicense: true, }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 6584c5891a8b9..f6cbc52e7a421 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -22,11 +22,12 @@ export const noopAlertType: AlertType = { name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() {}, producer: 'alerts', }; -export const alwaysFiringAlertType: any = { +export const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Always Firing', actionGroups: [ @@ -35,6 +36,7 @@ export const alwaysFiringAlertType: any = { ], defaultActionGroupId: 'default', producer: 'alerts', + minimumLicenseRequired: 'basic', async executor(alertExecutorOptions: any) { const { services, state, params } = alertExecutorOptions; @@ -52,7 +54,7 @@ export const alwaysFiringAlertType: any = { }, }; -export const failingAlertType: any = { +export const failingAlertType: AlertType = { id: 'test.failing', name: 'Test: Failing', actionGroups: [ @@ -63,6 +65,7 @@ export const failingAlertType: any = { ], producer: 'alerts', defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', async executor() { throw new Error('Failed to execute alert type'); },