* [Alerts][License] Define minimum license required for each alert type (#84997) * Define minimum license required for each alert type * fixed typechecks * fixed tests * fixed tests * fixed due to comments * fixed due to comments * removed file * removed casting to LicenseType * [Alerts][License] Add license checks to alerts HTTP APIs and execution (#85223) * [Alerts][License] Add license checks to alerts HTTP APIs and execution * fixed typechecks * resolved conflicts * resolved conflicts * added router tests * fixed typechecks * added license check support for alert task running * fixed typechecks * added integration tests * fixed due to comments * fixed due to comments * fixed tests * fixed typechecks * [Alerting UI][License] Disable alert types in UI when the license doesn't support it. (#85496) * [Alerting UI][License] Disable alert types in UI when the license doesn't support it. * fixed typechecks * added licensing for alert list and details page * fixed multy select menu * fixed due to comments * fixed due to comments * fixed due to comments * fixed typechecks * fixed license error message * fixed license error message * fixed typechecks * fixed license error message Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
/*
|
|
* 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 Boom from '@hapi/boom';
|
|
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 {
|
|
AlertType,
|
|
AlertTypeParams,
|
|
AlertTypeState,
|
|
AlertInstanceState,
|
|
AlertInstanceContext,
|
|
} from './types';
|
|
import { RecoveredActionGroup, getBuiltinActionGroups } from '../common';
|
|
import { ILicenseState } from './lib/license_state';
|
|
import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name';
|
|
|
|
export interface ConstructorOptions {
|
|
taskManager: TaskManagerSetupContract;
|
|
taskRunnerFactory: TaskRunnerFactory;
|
|
licenseState: ILicenseState;
|
|
licensing: LicensingPluginSetup;
|
|
}
|
|
|
|
export interface RegistryAlertType
|
|
extends Pick<
|
|
NormalizedAlertType,
|
|
| 'name'
|
|
| 'actionGroups'
|
|
| 'recoveryActionGroup'
|
|
| 'defaultActionGroupId'
|
|
| 'actionVariables'
|
|
| 'producer'
|
|
| 'minimumLicenseRequired'
|
|
> {
|
|
id: string;
|
|
enabledInLicense: boolean;
|
|
}
|
|
|
|
/**
|
|
* AlertType IDs are used as part of the authorization strings used to
|
|
* grant users privileged operations. There is a limited range of characters
|
|
* we can use in these auth strings, so we apply these same limitations to
|
|
* the AlertType Ids.
|
|
* If you wish to change this, please confer with the Kibana security team.
|
|
*/
|
|
const alertIdSchema = schema.string({
|
|
validate(value: string): string | void {
|
|
if (typeof value !== 'string') {
|
|
return `expected AlertType Id of type [string] but got [${typeDetect(value)}]`;
|
|
} else if (!value.match(/^[a-zA-Z0-9_\-\.]*$/)) {
|
|
const invalid = value.match(/[^a-zA-Z0-9_\-\.]+/g)!;
|
|
return `expected AlertType Id not to include invalid character${
|
|
invalid.length > 1 ? `s` : ``
|
|
}: ${invalid?.join(`, `)}`;
|
|
}
|
|
},
|
|
});
|
|
|
|
export type NormalizedAlertType<
|
|
Params extends AlertTypeParams = AlertTypeParams,
|
|
State extends AlertTypeState = AlertTypeState,
|
|
InstanceState extends AlertInstanceState = AlertInstanceState,
|
|
InstanceContext extends AlertInstanceContext = AlertInstanceContext
|
|
> = Omit<AlertType<Params, State, InstanceState, InstanceContext>, 'recoveryActionGroup'> &
|
|
Pick<Required<AlertType<Params, State, InstanceState, InstanceContext>>, 'recoveryActionGroup'>;
|
|
|
|
export class AlertTypeRegistry {
|
|
private readonly taskManager: TaskManagerSetupContract;
|
|
private readonly alertTypes: Map<string, NormalizedAlertType> = new Map();
|
|
private readonly taskRunnerFactory: TaskRunnerFactory;
|
|
private readonly licenseState: ILicenseState;
|
|
private readonly licensing: LicensingPluginSetup;
|
|
|
|
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,
|
|
InstanceState extends AlertInstanceState = AlertInstanceState,
|
|
InstanceContext extends AlertInstanceContext = AlertInstanceContext
|
|
>(alertType: AlertType<Params, State, InstanceState, InstanceContext>) {
|
|
if (this.has(alertType.id)) {
|
|
throw new Error(
|
|
i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', {
|
|
defaultMessage: 'Alert type "{id}" is already registered.',
|
|
values: {
|
|
id: alertType.id,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
|
|
|
|
const normalizedAlertType = augmentActionGroupsWithReserved(alertType as AlertType);
|
|
|
|
this.alertTypes.set(alertIdSchema.validate(alertType.id), normalizedAlertType);
|
|
this.taskManager.registerTaskDefinitions({
|
|
[`alerting:${alertType.id}`]: {
|
|
title: alertType.name,
|
|
createTaskRunner: (context: RunContext) =>
|
|
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<
|
|
Params extends AlertTypeParams = AlertTypeParams,
|
|
State extends AlertTypeState = AlertTypeState,
|
|
InstanceState extends AlertInstanceState = AlertInstanceState,
|
|
InstanceContext extends AlertInstanceContext = AlertInstanceContext
|
|
>(id: string): NormalizedAlertType<Params, State, InstanceState, InstanceContext> {
|
|
if (!this.has(id)) {
|
|
throw Boom.badRequest(
|
|
i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', {
|
|
defaultMessage: 'Alert type "{id}" is not registered.',
|
|
values: {
|
|
id,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
return this.alertTypes.get(id)! as NormalizedAlertType<
|
|
Params,
|
|
State,
|
|
InstanceState,
|
|
InstanceContext
|
|
>;
|
|
}
|
|
|
|
public list(): Set<RegistryAlertType> {
|
|
return new Set(
|
|
Array.from(this.alertTypes).map(
|
|
([
|
|
id,
|
|
{
|
|
name,
|
|
actionGroups,
|
|
recoveryActionGroup,
|
|
defaultActionGroupId,
|
|
actionVariables,
|
|
producer,
|
|
minimumLicenseRequired,
|
|
},
|
|
]: [string, NormalizedAlertType]) => ({
|
|
id,
|
|
name,
|
|
actionGroups,
|
|
recoveryActionGroup,
|
|
defaultActionGroupId,
|
|
actionVariables,
|
|
producer,
|
|
minimumLicenseRequired,
|
|
enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType(
|
|
id,
|
|
name,
|
|
minimumLicenseRequired
|
|
).isValid,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
function normalizedActionVariables(actionVariables: AlertType['actionVariables']) {
|
|
return {
|
|
context: actionVariables?.context ?? [],
|
|
state: actionVariables?.state ?? [],
|
|
params: actionVariables?.params ?? [],
|
|
};
|
|
}
|
|
|
|
function augmentActionGroupsWithReserved<
|
|
Params extends AlertTypeParams,
|
|
State extends AlertTypeState,
|
|
InstanceState extends AlertInstanceState,
|
|
InstanceContext extends AlertInstanceContext
|
|
>(
|
|
alertType: AlertType<Params, State, InstanceState, InstanceContext>
|
|
): NormalizedAlertType<Params, State, InstanceState, InstanceContext> {
|
|
const reservedActionGroups = getBuiltinActionGroups(alertType.recoveryActionGroup);
|
|
const { id, actionGroups, recoveryActionGroup } = alertType;
|
|
|
|
const activeActionGroups = new Set(actionGroups.map((item) => item.id));
|
|
const intersectingReservedActionGroups = intersection(
|
|
[...activeActionGroups.values()],
|
|
reservedActionGroups.map((item) => item.id)
|
|
);
|
|
if (recoveryActionGroup && activeActionGroups.has(recoveryActionGroup.id)) {
|
|
throw new Error(
|
|
i18n.translate(
|
|
'xpack.alerts.alertTypeRegistry.register.customRecoveryActionGroupUsageError',
|
|
{
|
|
defaultMessage:
|
|
'Alert type [id="{id}"] cannot be registered. Action group [{actionGroup}] cannot be used as both a recovery and an active action group.',
|
|
values: {
|
|
actionGroup: recoveryActionGroup.id,
|
|
id,
|
|
},
|
|
}
|
|
)
|
|
);
|
|
} else if (intersectingReservedActionGroups.length > 0) {
|
|
throw new Error(
|
|
i18n.translate('xpack.alerts.alertTypeRegistry.register.reservedActionGroupUsageError', {
|
|
defaultMessage:
|
|
'Alert type [id="{id}"] cannot be registered. Action groups [{actionGroups}] are reserved by the framework.',
|
|
values: {
|
|
actionGroups: intersectingReservedActionGroups.join(', '),
|
|
id,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
return {
|
|
...alertType,
|
|
actionGroups: [...actionGroups, ...reservedActionGroups],
|
|
recoveryActionGroup: recoveryActionGroup ?? RecoveredActionGroup,
|
|
};
|
|
}
|