[Alerting] Allow rule types to extract/inject saved object references on rule CRU (#101896)

* Adding function hooks into rule type definition and call extract fn on rule create

* Adding hooks for extracting and injecting saved object references. Adding extractReferences to create and update workflow

* Adding type template for extracted params

* Adding type template for extracted params

* Adding type template for extracted params

* Adding type template for extracted params

* Calling injectReferences function if defined. Finishing unit tests for create and update

* Adding tests for get

* Adding tests for find

* Cleanup

* Fixing types check

* Fixing functional tests

* Fixing functional tests

* Fixing tests

* Updating README

* Throwing boom error instead of normal error

* Adding framework level prefix to extracted saved object reference names

* Fixing types

* Fixing types

* PR fixes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
ymao1 2021-07-20 17:09:02 -04:00 committed by GitHub
parent 88ac1f9761
commit e2aacdc186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1339 additions and 46 deletions

View file

@ -39,6 +39,7 @@ function getTShirtSizeByIdAndThreshold(
export const alertType: AlertType<
AlwaysFiringParams,
never,
{ count?: number },
{ triggerdOnCycle: number },
never,

View file

@ -41,6 +41,7 @@ function getCraftFilter(craft: string) {
export const alertType: AlertType<
{ outerSpaceCapacity: number; craft: string; op: string },
never,
{ peopleInSpace: number },
{ craft: string },
never,

View file

@ -118,6 +118,8 @@ The following table describes the properties of the `options` object.
|executor|This is where the code for the rule type lives. This is a function to be called when executing a rule on an interval basis. For full details, see the executor section below.|Function|
|producer|The id of the application producing this rule type.|string|
|minimumLicenseRequired|The value of a minimum license. Most of the rules are licensed as "basic".|string|
|useSavedObjectReferences.extractReferences|(Optional) When developing a rule type, you can choose to implement hooks for extracting saved object references from rule parameters. This hook will be invoked when a rule is created or updated. Implementing this hook is optional, but if an extract hook is implemented, an inject hook must also be implemented.|Function
|useSavedObjectReferences.injectReferences|(Optional) When developing a rule type, you can choose to implement hooks for injecting saved object references into rule parameters. This hook will be invoked when a rule is retrieved (get or find). Implementing this hook is optional, but if an inject hook is implemented, an extract hook must also be implemented.|Function
|isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean|
### Executor
@ -173,6 +175,19 @@ For example, if the `context` has one variable `foo` which is an object that has
}
```
### useSavedObjectReferences Hooks
This is an optional pair of functions that can be implemented by a rule type. Both `extractReferences` and `injectReferences` functions must be implemented if either is impemented.
**useSavedObjectReferences.extractReferences**
This function should take the rule type params as input and extract out any saved object IDs stored within the params. For each saved object ID, a new saved object reference should be created and a saved object reference should replace the saved object ID in the rule params. This function should return the modified rule type params (with saved object reference name, not IDs) and an array of saved object references.
**useSavedObjectReferences.injectReferences**
This function should take the rule type params (with saved object references) and the saved object references array as input and inject the saved object ID in place of any saved object references in the rule type params. Note that any error thrown within this function will be propagated.
## Licensing
Currently most rule types are free features. But some rule types are subscription features, such as the tracking containment rule.
@ -210,6 +225,13 @@ import {
interface MyRuleTypeParams extends AlertTypeParams {
server: string;
threshold: number;
testSavedObjectId: string;
}
interface MyRuleTypeExtractedParams extends AlertTypeParams {
server: string;
threshold: number;
testSavedObjectRef: string;
}
interface MyRuleTypeState extends AlertTypeState {
@ -229,6 +251,7 @@ type MyRuleTypeActionGroups = 'default' | 'warning';
const myRuleType: AlertType<
MyRuleTypeParams,
MyRuleTypeExtractedParams,
MyRuleTypeState,
MyRuleTypeAlertState,
MyRuleTypeAlertContext,
@ -274,6 +297,7 @@ const myRuleType: AlertType<
rule,
}: AlertExecutorOptions<
MyRuleTypeParams,
MyRuleTypeExtractedParams,
MyRuleTypeState,
MyRuleTypeAlertState,
MyRuleTypeAlertContext,
@ -320,6 +344,29 @@ const myRuleType: AlertType<
};
},
producer: 'alerting',
useSavedObjectReferences: {
extractReferences: (params: Params): RuleParamsAndRefs<ExtractedParams> => {
const { testSavedObjectId, ...otherParams } = params;
const testSavedObjectRef = 'testRef_0';
const references = [
{
name: `testRef_0`,
id: testSavedObjectId,
type: 'index-pattern',
},
];
return { params: { ...otherParams, testSavedObjectRef }, references };
},
injectReferences: (params: SavedObjectAttributes, references: SavedObjectReference[]) => {
const { testSavedObjectRef, ...otherParams } = params;
const reference = references.find((ref) => ref.name === testSavedObjectRef);
if (!reference) {
throw new Error(`Test reference "${testSavedObjectRef}"`);
}
return { ...otherParams, testSavedObjectId: reference.id } as Params;
},
}
};
server.newPlatform.setup.plugins.alerting.registerType(myRuleType);

View file

@ -57,7 +57,7 @@ describe('has()', () => {
describe('register()', () => {
test('throws if AlertType Id contains invalid characters', () => {
const alertType: AlertType<never, never, never, never, 'default'> = {
const alertType: AlertType<never, never, never, never, never, 'default'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -90,7 +90,7 @@ describe('register()', () => {
});
test('throws if AlertType Id isnt a string', () => {
const alertType: AlertType<never, never, never, never, 'default'> = {
const alertType: AlertType<never, never, never, never, never, 'default'> = {
id: (123 as unknown) as string,
name: 'Test',
actionGroups: [
@ -113,7 +113,7 @@ describe('register()', () => {
});
test('throws if AlertType action groups contains reserved group id', () => {
const alertType: AlertType<never, never, never, never, 'default' | 'NotReserved'> = {
const alertType: AlertType<never, never, never, never, never, 'default' | 'NotReserved'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -146,7 +146,7 @@ describe('register()', () => {
});
test('allows an AlertType to specify a custom recovery group', () => {
const alertType: AlertType<never, never, never, never, 'default', 'backToAwesome'> = {
const alertType: AlertType<never, never, never, never, never, 'default', 'backToAwesome'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -187,6 +187,7 @@ describe('register()', () => {
never,
never,
never,
never,
'default' | 'backToAwesome',
'backToAwesome'
> = {
@ -222,7 +223,7 @@ describe('register()', () => {
});
test('registers the executor with the task manager', () => {
const alertType: AlertType<never, never, never, never, 'default'> = {
const alertType: AlertType<never, never, never, never, never, 'default'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -253,7 +254,7 @@ describe('register()', () => {
});
test('shallow clones the given alert type', () => {
const alertType: AlertType<never, never, never, never, 'default'> = {
const alertType: AlertType<never, never, never, never, never, 'default'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -506,8 +507,8 @@ function alertTypeWithVariables<ActionGroupIds extends string>(
id: ActionGroupIds,
context: string,
state: string
): AlertType<never, never, never, never, ActionGroupIds> {
const baseAlert: AlertType<never, never, never, never, ActionGroupIds> = {
): AlertType<never, never, never, never, never, ActionGroupIds> {
const baseAlert: AlertType<never, never, never, never, never, ActionGroupIds> = {
id,
name: `${id}-name`,
actionGroups: [],

View file

@ -74,6 +74,7 @@ const alertIdSchema = schema.string({
export type NormalizedAlertType<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -82,13 +83,22 @@ export type NormalizedAlertType<
> = {
actionGroups: Array<ActionGroup<ActionGroupIds | RecoveryActionGroupId>>;
} & Omit<
AlertType<Params, State, InstanceState, InstanceContext, ActionGroupIds, RecoveryActionGroupId>,
AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
ActionGroupIds,
RecoveryActionGroupId
>,
'recoveryActionGroup' | 'actionGroups'
> &
Pick<
Required<
AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -100,6 +110,7 @@ export type NormalizedAlertType<
>;
export type UntypedNormalizedAlertType = NormalizedAlertType<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,
@ -132,6 +143,7 @@ export class AlertTypeRegistry {
public register<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -140,6 +152,7 @@ export class AlertTypeRegistry {
>(
alertType: AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -161,6 +174,7 @@ export class AlertTypeRegistry {
const normalizedAlertType = augmentActionGroupsWithReserved<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -179,6 +193,7 @@ export class AlertTypeRegistry {
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -198,6 +213,7 @@ export class AlertTypeRegistry {
public get<
Params extends AlertTypeParams = AlertTypeParams,
ExtractedParams extends AlertTypeParams = AlertTypeParams,
State extends AlertTypeState = AlertTypeState,
InstanceState extends AlertInstanceState = AlertInstanceState,
InstanceContext extends AlertInstanceContext = AlertInstanceContext,
@ -207,6 +223,7 @@ export class AlertTypeRegistry {
id: string
): NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -230,6 +247,7 @@ export class AlertTypeRegistry {
*/
return (this.alertTypes.get(id)! as unknown) as NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -284,6 +302,7 @@ function normalizedActionVariables(actionVariables: AlertType['actionVariables']
function augmentActionGroupsWithReserved<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -292,6 +311,7 @@ function augmentActionGroupsWithReserved<
>(
alertType: AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -300,6 +320,7 @@ function augmentActionGroupsWithReserved<
>
): NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,

View file

@ -16,6 +16,7 @@ import {
SavedObject,
PluginInitializerContext,
SavedObjectsUtils,
SavedObjectAttributes,
} from '../../../../../src/core/server';
import { esKuery } from '../../../../../src/plugins/data/server';
import { ActionsClient, ActionsAuthorization } from '../../../actions/server';
@ -183,6 +184,9 @@ export interface GetAlertInstanceSummaryParams {
dateStart?: string;
}
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
const extractedSavedObjectParamReferenceNamePrefix = 'param:';
const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
type: AlertingAuthorizationFilterType.KQL,
fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },
@ -284,9 +288,14 @@ export class AlertsClient {
await this.validateActions(alertType, data.actions);
const createTime = Date.now();
const { references, actions } = await this.denormalizeActions(data.actions);
// Extract saved object references for this rule
const { references, params: updatedParams, actions } = await this.extractReferences(
alertType,
data.actions,
validatedAlertTypeParams
);
const createTime = Date.now();
const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle);
const rawAlert: RawAlert = {
@ -297,7 +306,7 @@ export class AlertsClient {
updatedBy: username,
createdAt: new Date(createTime).toISOString(),
updatedAt: new Date(createTime).toISOString(),
params: validatedAlertTypeParams as RawAlert['params'],
params: updatedParams as RawAlert['params'],
muteAll: false,
mutedInstanceIds: [],
notifyWhen,
@ -357,7 +366,12 @@ export class AlertsClient {
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
}
return this.getAlertFromRaw<Params>(createdAlert.id, createdAlert.attributes, references);
return this.getAlertFromRaw<Params>(
createdAlert.id,
createdAlert.attributes.alertTypeId,
createdAlert.attributes,
references
);
}
public async get<Params extends AlertTypeParams = never>({
@ -389,7 +403,12 @@ export class AlertsClient {
savedObject: { type: 'alert', id },
})
);
return this.getAlertFromRaw<Params>(result.id, result.attributes, result.references);
return this.getAlertFromRaw<Params>(
result.id,
result.attributes.alertTypeId,
result.attributes,
result.references
);
}
public async getAlertState({ id }: { id: string }): Promise<AlertTaskState | void> {
@ -518,6 +537,7 @@ export class AlertsClient {
}
return this.getAlertFromRaw<Params>(
id,
attributes.alertTypeId,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
references
);
@ -760,7 +780,13 @@ export class AlertsClient {
);
await this.validateActions(alertType, data.actions);
const { actions, references } = await this.denormalizeActions(data.actions);
// Extract saved object references for this rule
const { references, params: updatedParams, actions } = await this.extractReferences(
alertType,
data.actions,
validatedAlertTypeParams
);
const username = await this.getUserName();
let createdAPIKey = null;
@ -780,7 +806,7 @@ export class AlertsClient {
...attributes,
...data,
...apiKeyAttributes,
params: validatedAlertTypeParams as RawAlert['params'],
params: updatedParams as RawAlert['params'],
actions,
notifyWhen,
updatedBy: username,
@ -807,7 +833,12 @@ export class AlertsClient {
throw e;
}
return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
return this.getPartialAlertFromRaw(
id,
alertType,
updatedObject.attributes,
updatedObject.references
);
}
private apiKeyAsAlertAttributes(
@ -1436,18 +1467,29 @@ export class AlertsClient {
private getAlertFromRaw<Params extends AlertTypeParams>(
id: string,
ruleTypeId: string,
rawAlert: RawAlert,
references: SavedObjectReference[] | undefined
): Alert {
const ruleType = this.alertTypeRegistry.get(ruleTypeId);
// In order to support the partial update API of Saved Objects we have to support
// partial updates of an Alert, but when we receive an actual RawAlert, it is safe
// to cast the result to an Alert
return this.getPartialAlertFromRaw<Params>(id, rawAlert, references) as Alert;
return this.getPartialAlertFromRaw<Params>(id, ruleType, rawAlert, references) as Alert;
}
private getPartialAlertFromRaw<Params extends AlertTypeParams>(
id: string,
{ createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial<RawAlert>,
ruleType: UntypedNormalizedAlertType,
{
createdAt,
updatedAt,
meta,
notifyWhen,
scheduledTaskId,
params,
...rawAlert
}: Partial<RawAlert>,
references: SavedObjectReference[] | undefined
): PartialAlert<Params> {
// Not the prettiest code here, but if we want to use most of the
@ -1460,6 +1502,7 @@ export class AlertsClient {
};
delete rawAlertWithoutExecutionStatus.executionStatus;
const executionStatus = alertExecutionStatusFromRaw(this.logger, id, rawAlert.executionStatus);
return {
id,
notifyWhen,
@ -1470,6 +1513,7 @@ export class AlertsClient {
actions: rawAlert.actions
? this.injectReferencesIntoActions(id, rawAlert.actions, references || [])
: [],
params: this.injectReferencesIntoParams(id, ruleType, params, references || []) as Params,
...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
...(createdAt ? { createdAt: new Date(createdAt) } : {}),
...(scheduledTaskId ? { scheduledTaskId } : {}),
@ -1525,6 +1569,73 @@ export class AlertsClient {
}
}
private async extractReferences<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams
>(
ruleType: UntypedNormalizedAlertType,
ruleActions: NormalizedAlertAction[],
ruleParams: Params
): Promise<{
actions: RawAlert['actions'];
params: ExtractedParams;
references: SavedObjectReference[];
}> {
const { references: actionReferences, actions } = await this.denormalizeActions(ruleActions);
// Extracts any references using configured reference extractor if available
const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences
? ruleType.useSavedObjectReferences.extractReferences(ruleParams)
: null;
const extractedReferences = extractedRefsAndParams?.references ?? [];
const params = (extractedRefsAndParams?.params as ExtractedParams) ?? ruleParams;
// Prefix extracted references in order to avoid clashes with framework level references
const paramReferences = extractedReferences.map((reference: SavedObjectReference) => ({
...reference,
name: `${extractedSavedObjectParamReferenceNamePrefix}${reference.name}`,
}));
const references = [...actionReferences, ...paramReferences];
return {
actions,
params,
references,
};
}
private injectReferencesIntoParams<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams
>(
ruleId: string,
ruleType: UntypedNormalizedAlertType,
ruleParams: SavedObjectAttributes | undefined,
references: SavedObjectReference[]
): Params {
try {
const paramReferences = references
.filter((reference: SavedObjectReference) =>
reference.name.startsWith(extractedSavedObjectParamReferenceNamePrefix)
)
.map((reference: SavedObjectReference) => ({
...reference,
name: reference.name.replace(extractedSavedObjectParamReferenceNamePrefix, ''),
}));
return ruleParams && ruleType?.useSavedObjectReferences?.injectReferences
? (ruleType.useSavedObjectReferences.injectReferences(
ruleParams as ExtractedParams,
paramReferences
) as Params)
: (ruleParams as Params);
} catch (err) {
throw Boom.badRequest(
`Error injecting reference into rule params for rule id ${ruleId} - ${err.message}`
);
}
}
private async denormalizeActions(
alertActions: NormalizedAlertAction[]
): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> {

View file

@ -801,6 +801,360 @@ describe('create()', () => {
expect(taskManager.schedule).toHaveBeenCalledTimes(0);
});
test('should call useSavedObjectReferences.extractReferences and useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const ruleParams = {
bar: true,
parameterThatIsSavedObjectId: '9',
};
const extractReferencesFn = jest.fn().mockReturnValue({
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
references: [
{
name: 'soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
parameterThatIsSavedObjectId: '9',
});
alertTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: extractReferencesFn,
injectReferences: injectReferencesFn,
},
}));
const data = getMockData({
params: ruleParams,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: null,
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
taskManager.schedule.mockResolvedValueOnce({
id: 'task-123',
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {},
params: {},
ownerId: null,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
actions: [],
scheduledTaskId: 'task-123',
},
references: [],
});
const result = await alertsClient.create({ data });
expect(extractReferencesFn).toHaveBeenCalledWith(ruleParams);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{ actionRef: 'action_0', actionTypeId: 'test', group: 'default', params: { foo: true } },
],
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
},
meta: { versionApiKeyLastmodified: 'v7.10.0' },
muteAll: false,
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true, parameterThatIsSavedObjectRef: 'soRef_0' },
schedule: { interval: '10s' },
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: 'mock-saved-object-id',
references: [
{ id: '1', name: 'action_0', type: 'action' },
{ id: '9', name: 'param:soRef_0', type: 'someSavedObjectType' },
],
}
);
expect(injectReferencesFn).toHaveBeenCalledWith(
{
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
[{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }]
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": null,
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
},
"schedule": Object {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
});
test('should allow rule types to use action_ prefix for saved object reference names', async () => {
const ruleParams = {
bar: true,
parameterThatIsSavedObjectId: '8',
};
const extractReferencesFn = jest.fn().mockReturnValue({
params: {
bar: true,
parameterThatIsSavedObjectRef: 'action_0',
},
references: [
{
name: 'action_0',
type: 'someSavedObjectType',
id: '8',
},
],
});
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
parameterThatIsSavedObjectId: '8',
});
alertTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: extractReferencesFn,
injectReferences: injectReferencesFn,
},
}));
const data = getMockData({
params: ruleParams,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'action_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: null,
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:action_0',
type: 'someSavedObjectType',
id: '8',
},
],
});
taskManager.schedule.mockResolvedValueOnce({
id: 'task-123',
taskType: 'alerting:123',
scheduledAt: new Date(),
attempts: 1,
status: TaskStatus.Idle,
runAt: new Date(),
startedAt: null,
retryAt: null,
state: {},
params: {},
ownerId: null,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
actions: [],
scheduledTaskId: 'task-123',
},
references: [],
});
const result = await alertsClient.create({ data });
expect(extractReferencesFn).toHaveBeenCalledWith(ruleParams);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{ actionRef: 'action_0', actionTypeId: 'test', group: 'default', params: { foo: true } },
],
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
},
meta: { versionApiKeyLastmodified: 'v7.10.0' },
muteAll: false,
mutedInstanceIds: [],
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true, parameterThatIsSavedObjectRef: 'action_0' },
schedule: { interval: '10s' },
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: 'mock-saved-object-id',
references: [
{ id: '1', name: 'action_0', type: 'action' },
{ id: '8', name: 'param:action_0', type: 'someSavedObjectType' },
],
}
);
expect(injectReferencesFn).toHaveBeenCalledWith(
{
bar: true,
parameterThatIsSavedObjectRef: 'action_0',
},
[{ id: '8', name: 'action_0', type: 'someSavedObjectType' }]
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": null,
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "8",
},
"schedule": Object {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
});
test('should trim alert name when creating API key', async () => {
const data = getMockData({ name: ' my alert name ' });
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({

View file

@ -191,6 +191,335 @@ describe('find()', () => {
expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name');
});
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
jest.resetAllMocks();
authorization.getFindAuthorizationFilter.mockResolvedValue({
ensureRuleTypeIsAuthorized() {},
logSuccessfulAuthorization() {},
});
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
parameterThatIsSavedObjectId: '9',
});
alertTypeRegistry.list.mockReturnValue(
new Set([
...listedTypes,
{
actionGroups: [],
recoveryActionGroup: RecoveredActionGroup,
actionVariables: undefined,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
id: '123',
name: 'myType',
producer: 'myApp',
enabledInLicense: true,
},
])
);
alertTypeRegistry.get.mockImplementationOnce(() => ({
id: 'myType',
name: 'myType',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'myApp',
}));
alertTypeRegistry.get.mockImplementationOnce(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: jest.fn(),
injectReferences: injectReferencesFn,
},
}));
unsecuredSavedObjectsClient.find.mockResolvedValue({
total: 2,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
alertTypeId: 'myType',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
},
{
id: '2',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '20s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
},
],
});
const alertsClient = new AlertsClient(alertsClientParams);
const result = await alertsClient.find({ options: {} });
expect(injectReferencesFn).toHaveBeenCalledTimes(1);
expect(injectReferencesFn).toHaveBeenCalledWith(
{
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
[{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }]
);
expect(result).toMatchInlineSnapshot(`
Object {
"data": Array [
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "2",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
},
"schedule": Object {
"interval": "20s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
"page": 1,
"perPage": 10,
"total": 2,
}
`);
});
test('throws an error if useSavedObjectReferences.injectReferences throws an error', async () => {
jest.resetAllMocks();
authorization.getFindAuthorizationFilter.mockResolvedValue({
ensureRuleTypeIsAuthorized() {},
logSuccessfulAuthorization() {},
});
const injectReferencesFn = jest.fn().mockImplementation(() => {
throw new Error('something went wrong!');
});
alertTypeRegistry.list.mockReturnValue(
new Set([
...listedTypes,
{
actionGroups: [],
recoveryActionGroup: RecoveredActionGroup,
actionVariables: undefined,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
id: '123',
name: 'myType',
producer: 'myApp',
enabledInLicense: true,
},
])
);
alertTypeRegistry.get.mockImplementationOnce(() => ({
id: 'myType',
name: 'myType',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'myApp',
}));
alertTypeRegistry.get.mockImplementationOnce(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: jest.fn(),
injectReferences: injectReferencesFn,
},
}));
unsecuredSavedObjectsClient.find.mockResolvedValue({
total: 2,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
alertTypeId: 'myType',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
},
{
id: '2',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '20s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
},
],
});
const alertsClient = new AlertsClient(alertsClientParams);
await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error injecting reference into rule params for rule id 2 - something went wrong!"`
);
});
describe('authorization', () => {
test('ensures user is query filter types down to those the user is authorized to find', async () => {
const filter = esKuery.fromKueryExpression(
@ -257,6 +586,7 @@ describe('find()', () => {
"actions": Array [],
"id": "1",
"notifyWhen": undefined,
"params": undefined,
"schedule": undefined,
"tags": Array [
"myTag",

View file

@ -17,6 +17,7 @@ import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -118,6 +119,99 @@ describe('get()', () => {
`);
});
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
parameterThatIsSavedObjectId: '9',
});
alertTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: jest.fn(),
injectReferences: injectReferencesFn,
},
}));
const alertsClient = new AlertsClient(alertsClientParams);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const result = await alertsClient.get({ id: '1' });
expect(injectReferencesFn).toHaveBeenCalledWith(
{
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
[{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }]
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
},
"schedule": Object {
"interval": "10s",
},
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
});
test(`throws an error when references aren't found`, async () => {
const alertsClient = new AlertsClient(alertsClientParams);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
@ -146,6 +240,67 @@ describe('get()', () => {
);
});
test('throws an error if useSavedObjectReferences.injectReferences throws an error', async () => {
const injectReferencesFn = jest.fn().mockImplementation(() => {
throw new Error('something went wrong!');
});
alertTypeRegistry.get.mockImplementation(() => ({
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: jest.fn(),
injectReferences: injectReferencesFn,
},
}));
const alertsClient = new AlertsClient(alertsClientParams);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error injecting reference into rule params for rule id 1 - something went wrong!"`
);
});
describe('authorization', () => {
beforeEach(() => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({

View file

@ -24,6 +24,12 @@ import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
jest.mock('../../../../../../src/core/server/saved_objects/service/lib/utils', () => ({
SavedObjectsUtils: {
generateId: () => 'mock-saved-object-id',
},
}));
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
@ -401,6 +407,172 @@ describe('update()', () => {
expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true });
});
test('should call useSavedObjectReferences.extractReferences and useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const ruleParams = {
bar: true,
parameterThatIsSavedObjectId: '9',
};
const extractReferencesFn = jest.fn().mockReturnValue({
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
references: [
{
name: 'soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
parameterThatIsSavedObjectId: '9',
});
alertTypeRegistry.get.mockImplementation(() => ({
id: 'myType',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alerts',
useSavedObjectReferences: {
extractReferences: extractReferencesFn,
injectReferences: injectReferencesFn,
},
}));
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
enabled: true,
schedule: { interval: '10s' },
params: {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
notifyWhen: 'onActiveAlert',
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const result = await alertsClient.update({
id: '1',
data: {
schedule: { interval: '10s' },
name: 'abc',
tags: ['foo'],
params: ruleParams,
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});
expect(extractReferencesFn).toHaveBeenCalledWith(ruleParams);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{ actionRef: 'action_0', actionTypeId: 'test', group: 'default', params: { foo: true } },
],
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true, parameterThatIsSavedObjectRef: 'soRef_0' },
schedule: { interval: '10s' },
scheduledTaskId: 'task-123',
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: '1',
overwrite: true,
references: [
{ id: '1', name: 'action_0', type: 'action' },
{ id: '9', name: 'param:soRef_0', type: 'someSavedObjectType' },
],
version: '123',
}
);
expect(injectReferencesFn).toHaveBeenCalledWith(
{
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
[{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }]
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
"parameterThatIsSavedObjectId": "9",
},
"schedule": Object {
"interval": "10s",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
});
it('calls the createApiKey function', async () => {
alertsClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,

View file

@ -28,6 +28,7 @@ export type {
AlertInstanceState,
AlertInstanceContext,
AlertingApiRequestHandlerContext,
RuleParamsAndRefs,
} from './types';
export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config';
export { PluginSetupContract, PluginStartContract } from './plugin';

View file

@ -57,7 +57,7 @@ describe('getLicenseCheckForAlertType', () => {
let license: Subject<ILicense>;
let licenseState: ILicenseState;
const mockNotifyUsage = jest.fn();
const alertType: AlertType<never, never, never, never, 'default', 'recovered'> = {
const alertType: AlertType<never, never, never, never, never, 'default', 'recovered'> = {
id: 'test',
name: 'Test',
actionGroups: [
@ -192,7 +192,7 @@ describe('ensureLicenseForAlertType()', () => {
let license: Subject<ILicense>;
let licenseState: ILicenseState;
const mockNotifyUsage = jest.fn();
const alertType: AlertType<never, never, never, never, string, string> = {
const alertType: AlertType<never, never, never, never, never, string, string> = {
id: 'test',
name: 'Test',
actionGroups: [

View file

@ -140,6 +140,7 @@ export class LicenseState {
public ensureLicenseForAlertType<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -148,6 +149,7 @@ export class LicenseState {
>(
alertType: AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,

View file

@ -62,7 +62,7 @@ describe('Alerting Plugin', () => {
describe('registerType()', () => {
let setup: PluginSetupContract;
const sampleAlertType: AlertType<never, never, never, never, 'default'> = {
const sampleAlertType: AlertType<never, never, never, never, never, 'default'> = {
id: 'test',
name: 'test',
minimumLicenseRequired: 'basic',

View file

@ -87,6 +87,7 @@ export const LEGACY_EVENT_LOG_ACTIONS = {
export interface PluginSetupContract {
registerType<
Params extends AlertTypeParams = AlertTypeParams,
ExtractedParams extends AlertTypeParams = AlertTypeParams,
State extends AlertTypeState = AlertTypeState,
InstanceState extends AlertInstanceState = AlertInstanceState,
InstanceContext extends AlertInstanceContext = AlertInstanceContext,
@ -95,6 +96,7 @@ export interface PluginSetupContract {
>(
alertType: AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -277,6 +279,7 @@ export class AlertingPlugin {
return {
registerType<
Params extends AlertTypeParams = AlertTypeParams,
ExtractedParams extends AlertTypeParams = AlertTypeParams,
State extends AlertTypeState = AlertTypeState,
InstanceState extends AlertInstanceState = AlertInstanceState,
InstanceContext extends AlertInstanceContext = AlertInstanceContext,
@ -285,6 +288,7 @@ export class AlertingPlugin {
>(
alertType: AlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,

View file

@ -29,6 +29,7 @@ jest.mock('./inject_action_params', () => ({
}));
const alertType: NormalizedAlertType<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,
@ -59,6 +60,7 @@ const mockActionsPlugin = actionsMock.createStart();
const mockEventLogger = eventLoggerMock.create();
const createExecutionHandlerParams: jest.Mocked<
CreateExecutionHandlerOptions<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,

View file

@ -26,6 +26,7 @@ import { isEphemeralTaskRejectedDueToCapacityError } from '../../../task_manager
export interface CreateExecutionHandlerOptions<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -42,6 +43,7 @@ export interface CreateExecutionHandlerOptions<
kibanaBaseUrl: string | undefined;
alertType: NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -70,6 +72,7 @@ export type ExecutionHandler<ActionGroupIds extends string> = (
export function createExecutionHandler<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -93,6 +96,7 @@ export function createExecutionHandler<
maxEphemeralActionsPerAlert,
}: CreateExecutionHandlerOptions<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,

View file

@ -70,6 +70,7 @@ interface AlertTaskInstance extends ConcreteTaskInstance {
export class TaskRunner<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -81,6 +82,7 @@ export class TaskRunner<
private taskInstance: AlertTaskInstance;
private alertType: NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -92,6 +94,7 @@ export class TaskRunner<
constructor(
alertType: NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -171,6 +174,7 @@ export class TaskRunner<
) {
return createExecutionHandler<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -703,6 +707,7 @@ interface GenerateNewAndRecoveredInstanceEventsParams<
alertLabel: string;
namespace: string | undefined;
ruleType: NormalizedAlertType<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
{

View file

@ -59,6 +59,7 @@ export class TaskRunnerFactory {
public create<
Params extends AlertTypeParams,
ExtractedParams extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext,
@ -67,6 +68,7 @@ export class TaskRunnerFactory {
>(
alertType: NormalizedAlertType<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,
@ -81,6 +83,7 @@ export class TaskRunnerFactory {
return new TaskRunner<
Params,
ExtractedParams,
State,
InstanceState,
InstanceContext,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { IRouter, RequestHandlerContext } from 'src/core/server';
import type { IRouter, RequestHandlerContext, SavedObjectReference } from 'src/core/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { PublicAlertInstance } from './alert_instance';
import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
@ -99,6 +99,11 @@ export interface AlertExecutorOptions<
updatedBy: string | null;
}
export interface RuleParamsAndRefs<Params extends AlertTypeParams> {
references: SavedObjectReference[];
params: Params;
}
export type ExecutorType<
Params extends AlertTypeParams = never,
State extends AlertTypeState = never,
@ -114,6 +119,7 @@ export interface AlertTypeParamsValidator<Params extends AlertTypeParams> {
}
export interface AlertType<
Params extends AlertTypeParams = never,
ExtractedParams extends AlertTypeParams = never,
State extends AlertTypeState = never,
InstanceState extends AlertInstanceState = never,
InstanceContext extends AlertInstanceContext = never,
@ -146,6 +152,10 @@ export interface AlertType<
params?: ActionVariable[];
};
minimumLicenseRequired: LicenseType;
useSavedObjectReferences?: {
extractReferences: (params: Params) => RuleParamsAndRefs<ExtractedParams>;
injectReferences: (params: ExtractedParams, references: SavedObjectReference[]) => Params;
};
isExportable: boolean;
}

View file

@ -62,6 +62,7 @@ export const registerMetricInventoryThresholdAlertType = (
* TODO: Remove this use of `any` by utilizing a proper type
*/
Record<string, any>,
never, // Only use if defining useSavedObjectReferences hook
Record<string, any>,
AlertInstanceState,
AlertInstanceContext,

View file

@ -34,6 +34,7 @@ export const registerMetricAnomalyAlertType = (
* TODO: Remove this use of `any` by utilizing a proper type
*/
Record<string, any>,
never, // Only use if defining useSavedObjectReferences hook
Record<string, any>,
AlertInstanceState,
AlertInstanceContext,

View file

@ -38,6 +38,7 @@ export type MetricThresholdAlertType = AlertType<
* TODO: Remove this use of `any` by utilizing a proper type
*/
Record<string, any>,
never, // Only use if defining useSavedObjectReferences hook
Record<string, any>,
AlertInstanceState,
AlertInstanceContext,

View file

@ -46,6 +46,7 @@ export function registerAnomalyDetectionAlertType({
}: RegisterAlertParams) {
alerting.registerType<
MlAnomalyDetectionAlertParams,
never, // Only use if defining useSavedObjectReferences hook
AlertTypeState,
AlertInstanceState,
AnomalyDetectionAlertContext,

View file

@ -81,7 +81,7 @@ export class BaseAlert {
this.scopedLogger = Globals.app.getLogger(alertOptions.id);
}
public getAlertType(): AlertType<never, never, never, never, 'default'> {
public getAlertType(): AlertType<never, never, never, never, never, 'default'> {
const { id, name, actionVariables } = this.alertOptions;
return {
id,

View file

@ -18,7 +18,15 @@ import { AlertsClient } from './alert_data_client/alerts_client';
type SimpleAlertType<
TParams extends AlertTypeParams = {},
TAlertInstanceContext extends AlertInstanceContext = {}
> = AlertType<TParams, AlertTypeState, AlertInstanceState, TAlertInstanceContext, string, string>;
> = AlertType<
TParams,
TParams,
AlertTypeState,
AlertInstanceState,
TAlertInstanceContext,
string,
string
>;
export type AlertTypeExecutor<
TParams extends AlertTypeParams = {},
@ -35,7 +43,15 @@ export type AlertTypeWithExecutor<
TAlertInstanceContext extends AlertInstanceContext = {},
TServices extends Record<string, any> = {}
> = Omit<
AlertType<TParams, AlertTypeState, AlertInstanceState, TAlertInstanceContext, string, string>,
AlertType<
TParams,
TParams,
AlertTypeState,
AlertInstanceState,
TAlertInstanceContext,
string,
string
>,
'executor'
> & {
executor: AlertTypeExecutor<TParams, TAlertInstanceContext, TServices>;

View file

@ -101,12 +101,25 @@ export type NotificationExecutorOptions = AlertExecutorOptions<
// since we are only increasing the strictness of params.
export const isNotificationAlertExecutor = (
obj: NotificationAlertTypeDefinition
): obj is AlertType<AlertTypeParams, AlertTypeState, AlertInstanceState, AlertInstanceContext> => {
): obj is AlertType<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,
AlertInstanceContext
> => {
return true;
};
export type NotificationAlertTypeDefinition = Omit<
AlertType<AlertTypeParams, AlertTypeState, AlertInstanceState, AlertInstanceContext, 'default'>,
AlertType<
AlertTypeParams,
AlertTypeParams,
AlertTypeState,
AlertInstanceState,
AlertInstanceContext,
'default'
>,
'executor'
> & {
executor: ({

View file

@ -184,6 +184,7 @@ export const isAlertExecutor = (
obj: SignalRuleAlertTypeDefinition
): obj is AlertType<
RuleParams,
never, // Only use if defining useSavedObjectReferences hook
AlertTypeState,
AlertInstanceState,
AlertInstanceContext,
@ -194,6 +195,7 @@ export const isAlertExecutor = (
export type SignalRuleAlertTypeDefinition = AlertType<
RuleParams,
never, // Only use if defining useSavedObjectReferences hook
AlertTypeState,
AlertInstanceState,
AlertInstanceContext,

View file

@ -27,7 +27,14 @@ export const ConditionMetAlertInstanceId = 'query matched';
export function getAlertType(
logger: Logger
): AlertType<EsQueryAlertParams, EsQueryAlertState, {}, ActionContext, typeof ActionGroupId> {
): AlertType<
EsQueryAlertParams,
never, // Only use if defining useSavedObjectReferences hook
EsQueryAlertState,
{},
ActionContext,
typeof ActionGroupId
> {
const alertTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', {
defaultMessage: 'Elasticsearch query',
});

View file

@ -140,6 +140,7 @@ export interface GeoContainmentInstanceContext extends AlertInstanceContext {
export type GeoContainmentAlertType = AlertType<
GeoContainmentParams,
never, // Only use if defining useSavedObjectReferences hook
GeoContainmentState,
GeoContainmentInstanceState,
GeoContainmentInstanceContext,

View file

@ -26,6 +26,7 @@ export function register(params: RegisterParams) {
const { logger, alerting } = params;
alerting.registerType<
GeoContainmentParams,
never, // Only use if defining useSavedObjectReferences hook
GeoContainmentState,
GeoContainmentInstanceState,
GeoContainmentInstanceContext,

View file

@ -23,7 +23,7 @@ export const ActionGroupId = 'threshold met';
export function getAlertType(
logger: Logger,
data: Promise<StackAlertsStartDeps['triggersActionsUi']['data']>
): AlertType<Params, {}, {}, ActionContext, typeof ActionGroupId> {
): AlertType<Params, never, {}, {}, ActionContext, typeof ActionGroupId> {
const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', {
defaultMessage: 'Index threshold',
});

View file

@ -11,6 +11,7 @@ import { PluginSetupContract as AlertingSetup } from '../../alerting/server';
export {
PluginSetupContract as AlertingSetup,
AlertType,
RuleParamsAndRefs,
AlertExecutorOptions,
} from '../../alerting/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';

View file

@ -64,6 +64,7 @@ function getAlwaysFiringAlertType() {
}
const result: AlertType<
ParamsType & AlertTypeParams,
never, // Only use if defining useSavedObjectReferences hook
State,
InstanceState,
InstanceContext,
@ -158,7 +159,7 @@ function getCumulativeFiringAlertType() {
interface InstanceState extends AlertInstanceState {
instanceStateValue: boolean;
}
const result: AlertType<{}, State, InstanceState, {}, 'default' | 'other'> = {
const result: AlertType<{}, {}, State, InstanceState, {}, 'default' | 'other'> = {
id: 'test.cumulative-firing',
name: 'Test: Cumulative Firing',
actionGroups: [
@ -199,7 +200,7 @@ function getNeverFiringAlertType() {
interface State extends AlertTypeState {
globalStateValue: boolean;
}
const result: AlertType<ParamsType, State, {}, {}, 'default'> = {
const result: AlertType<ParamsType, never, State, {}, {}, 'default'> = {
id: 'test.never-firing',
name: 'Test: Never firing',
actionGroups: [
@ -240,7 +241,7 @@ function getFailingAlertType() {
reference: schema.string(),
});
type ParamsType = TypeOf<typeof paramsSchema>;
const result: AlertType<ParamsType, {}, {}, {}, 'default'> = {
const result: AlertType<ParamsType, never, {}, {}, {}, 'default'> = {
id: 'test.failing',
name: 'Test: Failing',
validate: {
@ -282,7 +283,7 @@ function getAuthorizationAlertType(core: CoreSetup<FixtureStartDeps>) {
reference: schema.string(),
});
type ParamsType = TypeOf<typeof paramsSchema>;
const result: AlertType<ParamsType, {}, {}, {}, 'default'> = {
const result: AlertType<ParamsType, never, {}, {}, {}, 'default'> = {
id: 'test.authorization',
name: 'Test: Authorization',
actionGroups: [
@ -370,7 +371,7 @@ function getValidationAlertType() {
param1: schema.string(),
});
type ParamsType = TypeOf<typeof paramsSchema>;
const result: AlertType<ParamsType, {}, {}, {}, 'default'> = {
const result: AlertType<ParamsType, never, {}, {}, {}, 'default'> = {
id: 'test.validation',
name: 'Test: Validation',
actionGroups: [
@ -403,7 +404,7 @@ function getPatternFiringAlertType() {
interface State extends AlertTypeState {
patternIndex?: number;
}
const result: AlertType<ParamsType, State, {}, {}, 'default'> = {
const result: AlertType<ParamsType, never, State, {}, {}, 'default'> = {
id: 'test.patternFiring',
name: 'Test: Firing on a Pattern',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -468,7 +469,7 @@ export function defineAlertTypes(
core: CoreSetup<FixtureStartDeps>,
{ alerting }: Pick<FixtureSetupDeps, 'alerting'>
) {
const noopAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const noopAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -478,7 +479,7 @@ export function defineAlertTypes(
isExportable: true,
async executor() {},
};
const goldNoopAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const goldNoopAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.gold.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -488,7 +489,7 @@ export function defineAlertTypes(
isExportable: true,
async executor() {},
};
const onlyContextVariablesAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const onlyContextVariablesAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.onlyContextVariables',
name: 'Test: Only Context Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -501,7 +502,7 @@ export function defineAlertTypes(
},
async executor() {},
};
const onlyStateVariablesAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const onlyStateVariablesAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.onlyStateVariables',
name: 'Test: Only State Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -514,7 +515,7 @@ export function defineAlertTypes(
isExportable: true,
async executor() {},
};
const throwAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const throwAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.throw',
name: 'Test: Throw',
actionGroups: [
@ -531,7 +532,7 @@ export function defineAlertTypes(
throw new Error('this alert is intended to fail');
},
};
const longRunningAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const longRunningAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.longRunning',
name: 'Test: Long Running',
actionGroups: [
@ -548,6 +549,27 @@ export function defineAlertTypes(
await new Promise((resolve) => setTimeout(resolve, 5000));
},
};
const exampleAlwaysFiringAlertType: AlertType<
{},
{},
{},
{},
{},
'small' | 'medium' | 'large'
> = {
id: 'example.always-firing',
name: 'Always firing',
actionGroups: [
{ id: 'small', name: 'Small t-shirt' },
{ id: 'medium', name: 'Medium t-shirt' },
{ id: 'large', name: 'Large t-shirt' },
],
defaultActionGroupId: 'small',
minimumLicenseRequired: 'basic',
isExportable: true,
async executor() {},
producer: 'alertsFixture',
};
alerting.registerType(getAlwaysFiringAlertType());
alerting.registerType(getCumulativeFiringAlertType());
@ -562,4 +584,5 @@ export function defineAlertTypes(
alerting.registerType(throwAlertType);
alerting.registerType(longRunningAlertType);
alerting.registerType(goldNoopAlertType);
alerting.registerType(exampleAlwaysFiringAlertType);
}

View file

@ -13,7 +13,7 @@ export function defineAlertTypes(
core: CoreSetup<FixtureStartDeps>,
{ alerting }: Pick<FixtureSetupDeps, 'alerting'>
) {
const noopRestrictedAlertType: AlertType<{}, {}, {}, {}, 'default', 'restrictedRecovered'> = {
const noopRestrictedAlertType: AlertType<{}, {}, {}, {}, {}, 'default', 'restrictedRecovered'> = {
id: 'test.restricted-noop',
name: 'Test: Restricted Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -24,7 +24,7 @@ export function defineAlertTypes(
recoveryActionGroup: { id: 'restrictedRecovered', name: 'Restricted Recovery' },
async executor() {},
};
const noopUnrestrictedAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
const noopUnrestrictedAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.unrestricted-noop',
name: 'Test: Unrestricted Noop',
actionGroups: [{ id: 'default', name: 'Default' }],

View file

@ -18,7 +18,7 @@ export interface AlertingExampleDeps {
features: FeaturesPluginSetup;
}
export const noopAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
export const noopAlertType: AlertType<{}, {}, {}, {}, {}, 'default'> = {
id: 'test.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
@ -31,6 +31,7 @@ export const noopAlertType: AlertType<{}, {}, {}, {}, 'default'> = {
export const alwaysFiringAlertType: AlertType<
{ instances: Array<{ id: string; state: any }> },
never, // Only use if defining useSavedObjectReferences hook
{
globalStateValue: boolean;
groupInSeriesIndex: number;
@ -66,7 +67,7 @@ export const alwaysFiringAlertType: AlertType<
},
};
export const failingAlertType: AlertType<never, never, never, never, 'default' | 'other'> = {
export const failingAlertType: AlertType<never, never, never, never, never, 'default' | 'other'> = {
id: 'test.failing',
name: 'Test: Failing',
actionGroups: [