From a57a0254edb6a35706cb6990e12638796c4449e6 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 1 Apr 2021 12:59:16 -0700 Subject: [PATCH 1/2] wip - ignore --- x-pack/plugins/rule_registry/README.md | 115 ++ x-pack/plugins/rule_registry/server/plugin.ts | 101 +- .../server/rac_client/rac_client.ts | 1510 +++++++++++++++++ .../server/rac_client/rac_client_factory.ts | 135 ++ x-pack/plugins/rule_registry/server/types.ts | 14 + .../feature_privilege_builder/index.ts | 1 + 6 files changed, 1859 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/rac_client/rac_client.ts create mode 100644 x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 17fe2b20f74fa..eca6a93ae8806 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -1,3 +1,19 @@ +# RAC + +The RAC plugin provides a common place to register rules with alerting. You can: + +- Register types of rules +- Perform CRUD actions on rules +- Perform CRUD actions on alerts produced by rules + +---- + +Table of Contents + +- [Rule Registry](#rule-registry) +- [Role Based Access-Control](#rbac) + +## Rule Registry The rule registry plugin aims to make it easy for rule type producers to have their rules produce the data that they need to build rich experiences on top of a unified experience, without the risk of mapping conflicts. A rule registry creates a template, an ILM policy, and an alias. The template mappings can be configured. It also injects a client scoped to these indices. @@ -66,3 +82,102 @@ The following fields are available in the root rule registry: - `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting. This list is not final - just a start. Field names might change or moved to a scoped registry. If we implement log and sequence based rule types the list of fields will grow. If a rule type needs additional fields, the recommendation would be to have the field in its own registry first (or in its producer’s registry), and if usage is more broadly adopted, it can be moved to the root registry. + +## Role Based Access-Control + +Rules registered through the rule registry produce `alerts` that are indexed into the `.alerts` index. Using the `producer` defined in the rule registry, these alerts inheret the `producer` property which is used in the auth to determine whether a user has access to these alerts and what operations they can perform on them. + +Users will need to be granted access to these `alerts`. When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. Assuming your feature generates `alerts`, you'll want to control which roles have all/read privileges for these alerts that are scoped to your feature. For example, the `security_solution` plugin allows users to create rules that generate `alerts`, so does `observability`. The `security_solution` plugin only wants to grant it's users access to `alerts` belonging to `security_solution`. However, a user may have access to numerous `alerts` like `['security_solution', 'observability']`. + +You can control all of these abilities by assigning privileges to Alerts from within your own feature, for example: + +```typescript +features.registerKibanaFeature({ + id: 'my-application-id', + name: 'My Application', + app: [], + privileges: { + all: { + alerts: { + all: [ + // grant `all` over our own types + 'my-application-id.my-feature', + 'my-application-id.my-restricted-alert-type', + // grant `all` over the built-in IndexThreshold + '.index-threshold', + // grant `all` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], + }, + }, + read: { + alerts: { + read: [ + // grant `read` over our own type + 'my-application-id.my-feature', + // grant `read` over the built-in IndexThreshold + '.index-threshold', + // grant `read` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], + }, + }, + }, +}); +``` + +In this example we can see the following: +- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts. +- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type. +- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. + +It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: + +```typescript +features.registerKibanaFeature({ + id: 'my-application-id', + name: 'My Application', + app: [], + privileges: { + all: { + app: ['my-application-id', 'kibana'], + savedObject: { + all: [], + read: [], + }, + ui: [], + api: [], + }, + read: { + app: ['lens', 'kibana'], + alerting: { + all: [ + 'my-application-id.my-alert-type' + ], + read: [ + 'my-application-id.my-restricted-alert-type' + ], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + api: [], + }, + }, +}); +``` + +In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType. +As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it. + +### `read` privileges vs. `all` privileges +When a user is granted the `read` role in for Alerts, they will be able to execute the following api calls: +- `get` +- `find` + +When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls: +- `update` + +Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error thrown by the AlertsClient. \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 06c16302b4b0a..e533baf89499d 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -5,45 +5,112 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { PluginInitializerContext, Plugin, CoreSetup, SharedGlobalConfig } from 'src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../alerting/server'; import { RuleRegistry } from './rule_registry'; import { defaultIlmPolicy } from './rule_registry/defaults/ilm_policy'; import { defaultFieldMap } from './rule_registry/defaults/field_map'; -import { RuleRegistryConfig } from '.'; +import { RacClientFactory } from './rac_client/rac_client'; +export interface RacPluginsSetup { + security?: SecurityPluginSetup; + alerting: AlertingPluginSetupContract; +} +export interface RacPluginsStart { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; + alerting: AlertingPluginStartContract; +} + +export type RacPluginSetupContract = RuleRegistry; -export type RuleRegistryPluginSetupContract = RuleRegistry; +export class RuleRegistryPlugin implements Plugin { + private readonly config: Promise; + private readonly racClientFactory: RacClientFactory; + private security?: SecurityPluginSetup; + private readonly logger: Logger; + private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; -export class RuleRegistryPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { + this.config = initContext.config.legacy.get(); this.initContext = initContext; + this.racClientFactory = new RacClientFactory(); + this.logger = initContext.logger.get('root'); + this.kibanaVersion = initContext.env.packageInfo.version; } - public setup( - core: CoreSetup, - plugins: { alerting: AlertingPluginSetupContract } - ): RuleRegistryPluginSetupContract { - const globalConfig = this.initContext.config.legacy.get(); - const config = this.initContext.config.get(); - - const logger = this.initContext.logger.get(); + public setup(core: CoreSetup, plugins: RacPluginsSetup): RacPluginSetupContract { + this.security = plugins.security; + // RULE REGISTRY const rootRegistry = new RuleRegistry({ core, ilmPolicy: defaultIlmPolicy, fieldMap: defaultFieldMap, - kibanaIndex: globalConfig.kibana.index, - name: 'alert-history', - kibanaVersion: this.initContext.env.packageInfo.version, - logger: logger.get('root'), + kibanaIndex: this.config.kibana.index, + namespace: 'alert-history', + kibanaVersion: this.kibanaVersion, + logger: this.logger, alertingPluginSetupContract: plugins.alerting, writeEnabled: config.writeEnabled, }); + // ALERTS ROUTES + core.http.registerRouteHandlerContext( + 'rac', + this.createRouteHandlerContext(core) + ); + return rootRegistry; } - public start() {} + public start(core: CoreStart, plugins: RacPluginsStart) { + const { logger, licenseState, security, racClientFactory } = this; + + racClientFactory.initialize({ + logger, + securityPluginSetup: security, + securityPluginStart: plugins.security, + getSpaceId(request: KibanaRequest) { + return plugins.spaces?.spacesService.getSpaceId(request); + }, + async getSpace(request: KibanaRequest) { + return plugins.spaces?.spacesService.getActiveSpace(request); + }, + features: plugins.features, + kibanaVersion: this.kibanaVersion, + }); + + const getRacClientWithRequest = (request: KibanaRequest) => { + if (isESOCanEncrypt !== true) { + throw new Error( + `Unable to create rac client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + ); + } + return racClientFactory!.create(request, core.savedObjects); + }; + + return { + getRacClientWithRequest, + }; + } + + private createRouteHandlerContext = ( + core: CoreSetup + ): IContextProvider => { + const { alertTypeRegistry, alertsClientFactory } = this; + return async function alertsRouteHandlerContext(context, request) { + const [{ savedObjects }] = await core.getStartServices(); + return { + getAlertsClient: () => { + return alertsClientFactory!.create(request, savedObjects); + }, + listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!), + getFrameworkHealth: async () => + await getHealth(savedObjects.createInternalRepository(['alert'])), + }; + }; + }; public stop() {} } diff --git a/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts b/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts new file mode 100644 index 0000000000000..7bfb6bd5dde02 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts @@ -0,0 +1,1510 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { estypes } from '@elastic/elasticsearch'; +import { + Logger, + SavedObjectsClientContract, + SavedObjectReference, + SavedObject, + PluginInitializerContext, + SavedObjectsUtils, +} from '../../../../../src/core/server'; +import { esKuery } from '../../../../../src/plugins/data/server'; +import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; +import { + Alert, + PartialAlert, + RawAlert, + AlertTypeRegistry, + AlertAction, + IntervalSchedule, + SanitizedAlert, + AlertTaskState, + AlertInstanceSummary, + AlertExecutionStatusValues, + AlertNotifyWhenType, + AlertTypeParams, +} from '../types'; +import { + validateAlertTypeParams, + alertExecutionStatusFromRaw, + getAlertNotifyWhenType, +} from '../lib'; +import { + GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, + InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, +} from '../../../security/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../../task_manager/server'; +import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; +import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry'; +import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization'; +import { IEventLogClient } from '../../../../plugins/event_log/server'; +import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; +import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; +import { IEvent } from '../../../event_log/server'; +import { AuditLogger, EventOutcome } from '../../../security/server'; +import { parseDuration } from '../../common/parse_duration'; +import { retryIfConflicts } from '../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../saved_objects'; +import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { mapSortField } from './lib'; + +export interface RegistryAlertTypeWithAuth extends RegistryAlertType { + authorizedConsumers: string[]; +} +type NormalizedAlertAction = Omit; +export type CreateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; +export type InvalidateAPIKeyResult = + | { apiKeysEnabled: false } + | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult }; + +export interface ConstructorOptions { + logger: Logger; + taskManager: TaskManagerStartContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; + authorization: AlertsAuthorization; + actionsAuthorization: ActionsAuthorization; + alertTypeRegistry: AlertTypeRegistry; + encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + spaceId?: string; + namespace?: string; + getUserName: () => Promise; + createAPIKey: (name: string) => Promise; + getActionsClient: () => Promise; + getEventLogClient: () => Promise; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + auditLogger?: AuditLogger; +} + +export interface MuteOptions extends IndexType { + alertId: string; + alertInstanceId: string; +} + +export interface FindOptions extends IndexType { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + sortOrder?: estypes.SortOrder; + hasReference?: { + type: string; + id: string; + }; + fields?: string[]; + filter?: string; +} + +export interface AggregateOptions extends IndexType { + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + hasReference?: { + type: string; + id: string; + }; + filter?: string; +} + +interface IndexType { + [key: string]: unknown; +} + +export interface AggregateResult { + alertExecutionStatus: { [status: string]: number }; +} + +export interface FindResult { + page: number; + perPage: number; + total: number; + data: Array>; +} + +export interface CreateOptions { + data: Omit< + Alert, + | 'id' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'apiKey' + | 'apiKeyOwner' + | 'muteAll' + | 'mutedInstanceIds' + | 'actions' + | 'executionStatus' + > & { actions: NormalizedAlertAction[] }; + options?: { + id?: string; + migrationVersion?: Record; + }; +} + +export interface UpdateOptions { + id: string; + data: { + name: string; + tags: string[]; + schedule: IntervalSchedule; + actions: NormalizedAlertAction[]; + params: Params; + throttle: string | null; + notifyWhen: AlertNotifyWhenType | null; + }; +} + +export interface GetAlertInstanceSummaryParams { + id: string; + dateStart?: string; +} + +export class RacClient { + private readonly logger: Logger; + private readonly getUserName: () => Promise; + private readonly spaceId?: string; + private readonly namespace?: string; + private readonly taskManager: TaskManagerStartContract; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + private readonly authorization: AlertsAuthorization; + private readonly alertTypeRegistry: AlertTypeRegistry; + private readonly createAPIKey: (name: string) => Promise; + private readonly getActionsClient: () => Promise; + private readonly actionsAuthorization: ActionsAuthorization; + private readonly getEventLogClient: () => Promise; + private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; + private readonly auditLogger?: AuditLogger; + + constructor({ + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization, + taskManager, + logger, + spaceId, + namespace, + getUserName, + createAPIKey, + encryptedSavedObjectsClient, + getActionsClient, + actionsAuthorization, + getEventLogClient, + kibanaVersion, + auditLogger, + }: ConstructorOptions) { + this.logger = logger; + this.getUserName = getUserName; + this.spaceId = spaceId; + this.namespace = namespace; + this.taskManager = taskManager; + this.alertTypeRegistry = alertTypeRegistry; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; + this.authorization = authorization; + this.createAPIKey = createAPIKey; + this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; + this.getActionsClient = getActionsClient; + this.actionsAuthorization = actionsAuthorization; + this.getEventLogClient = getEventLogClient; + this.kibanaVersion = kibanaVersion; + this.auditLogger = auditLogger; + } + + public async create({ + data, + options, + }: CreateOptions): Promise> { + const id = options?.id || SavedObjectsUtils.generateId(); + + try { + await this.authorization.ensureAuthorized( + data.alertTypeId, + data.consumer, + WriteOperations.Create + ); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.CREATE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId); + + // Throws an error if alert type isn't registered + const alertType = this.alertTypeRegistry.get(data.alertTypeId); + + const validatedAlertTypeParams = validateAlertTypeParams( + data.params, + alertType.validate?.params + ); + const username = await this.getUserName(); + + const createdAPIKey = data.enabled + ? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name)) + : null; + + this.validateActions(alertType, data.actions); + + const createTime = Date.now(); + const { references, actions } = await this.denormalizeActions(data.actions); + + const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle); + + const rawAlert: RawAlert = { + ...data, + ...this.apiKeyAsAlertAttributes(createdAPIKey, username), + actions, + createdBy: username, + updatedBy: username, + createdAt: new Date(createTime).toISOString(), + updatedAt: new Date(createTime).toISOString(), + params: validatedAlertTypeParams as RawAlert['params'], + muteAll: false, + mutedInstanceIds: [], + notifyWhen, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date().toISOString(), + error: null, + }, + }; + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + let createdAlert: SavedObject; + try { + createdAlert = await this.unsecuredSavedObjectsClient.create( + 'alert', + this.updateMeta(rawAlert), + { + ...options, + references, + id, + } + ); + } catch (e) { + // Avoid unused API key + markApiKeyForInvalidation( + { apiKey: rawAlert.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); + throw e; + } + if (data.enabled) { + let scheduledTask; + try { + scheduledTask = await this.scheduleAlert( + createdAlert.id, + rawAlert.alertTypeId, + data.schedule + ); + } catch (e) { + // Cleanup data, something went wrong scheduling the task + try { + await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); + } catch (err) { + // Skip the cleanup error and throw the task manager error to avoid confusion + this.logger.error( + `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}` + ); + } + throw e; + } + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + scheduledTaskId: scheduledTask.id, + }); + createdAlert.attributes.scheduledTaskId = scheduledTask.id; + } + return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); + } + + public async get({ + id, + }: { + id: string; + }): Promise> { + const result = await this.unsecuredSavedObjectsClient.get('alert', id); + try { + await this.authorization.ensureAuthorized( + result.attributes.alertTypeId, + result.attributes.consumer, + ReadOperations.Get + ); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + savedObject: { type: 'alert', id }, + }) + ); + return this.getAlertFromRaw(result.id, result.attributes, result.references); + } + + public async getAlertState({ id }: { id: string }): Promise { + const alert = await this.get({ id }); + await this.authorization.ensureAuthorized( + alert.alertTypeId, + alert.consumer, + ReadOperations.GetAlertState + ); + if (alert.scheduledTaskId) { + const { state } = taskInstanceToAlertTaskInstance( + await this.taskManager.get(alert.scheduledTaskId), + alert + ); + return state; + } + } + + public async getAlertInstanceSummary({ + id, + dateStart, + }: GetAlertInstanceSummaryParams): Promise { + this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); + const alert = await this.get({ id }); + await this.authorization.ensureAuthorized( + alert.alertTypeId, + alert.consumer, + ReadOperations.GetAlertInstanceSummary + ); + + // default duration of instance summary is 60 * alert interval + const dateNow = new Date(); + const durationMillis = parseDuration(alert.schedule.interval) * 60; + const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); + const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); + + const eventLogClient = await this.getEventLogClient(); + + this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); + let events: IEvent[]; + try { + const queryResults = await eventLogClient.findEventsBySavedObjectIds('alert', [id], { + page: 1, + per_page: 10000, + start: parsedDateStart.toISOString(), + end: dateNow.toISOString(), + sort_order: 'desc', + }); + events = queryResults.data; + } catch (err) { + this.logger.debug( + `alertsClient.getAlertInstanceSummary(): error searching event log for alert ${id}: ${err.message}` + ); + events = []; + } + + return alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart: parsedDateStart.toISOString(), + dateEnd: dateNow.toISOString(), + }); + } + + public async find({ + options: { fields, ...options } = {}, + }: { options?: FindOptions } = {}): Promise> { + let authorizationTuple; + try { + authorizationTuple = await this.authorization.getFindAuthorizationFilter(); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + error, + }) + ); + throw error; + } + const { + filter: authorizationFilter, + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + } = authorizationTuple; + + const { + page, + per_page: perPage, + total, + saved_objects: data, + } = await this.unsecuredSavedObjectsClient.find({ + ...options, + sortField: mapSortField(options.sortField), + filter: + (authorizationFilter && options.filter + ? nodeBuilder.and([esKuery.fromKueryExpression(options.filter), authorizationFilter]) + : authorizationFilter) ?? options.filter, + fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, + type: 'alert', + }); + + const authorizedData = data.map(({ id, attributes, references }) => { + try { + ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + return this.getAlertFromRaw( + id, + fields ? (pick(attributes, fields) as RawAlert) : attributes, + references + ); + }); + + authorizedData.forEach(({ id }) => + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.FIND, + savedObject: { type: 'alert', id }, + }) + ) + ); + + logSuccessfulAuthorization(); + + return { + page, + perPage, + total, + data: authorizedData, + }; + } + + public async aggregate({ + options: { fields, ...options } = {}, + }: { options?: AggregateOptions } = {}): Promise { + // Replace this when saved objects supports aggregations https://github.com/elastic/kibana/pull/64002 + const alertExecutionStatus = await Promise.all( + AlertExecutionStatusValues.map(async (status: string) => { + const { + filter: authorizationFilter, + logSuccessfulAuthorization, + } = await this.authorization.getFindAuthorizationFilter(); + const filter = options.filter + ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` + : `alert.attributes.executionStatus.status:(${status})`; + const { total } = await this.unsecuredSavedObjectsClient.find({ + ...options, + filter: + (authorizationFilter && filter + ? nodeBuilder.and([esKuery.fromKueryExpression(filter), authorizationFilter]) + : authorizationFilter) ?? filter, + page: 1, + perPage: 0, + type: 'alert', + }); + + logSuccessfulAuthorization(); + + return { [status]: total }; + }) + ); + + return { + alertExecutionStatus: alertExecutionStatus.reduce( + (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), + {} + ), + }; + } + + public async delete({ id }: { id: string }) { + let taskIdToRemove: string | undefined | null; + let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; + + try { + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; + attributes = decryptedAlert.attributes; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the scheduledTaskId using SOC + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + taskIdToRemove = alert.attributes.scheduledTaskId; + attributes = alert.attributes; + } + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.Delete + ); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.DELETE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.DELETE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); + + await Promise.all([ + taskIdToRemove ? this.taskManager.removeIfExists(taskIdToRemove) : null, + apiKeyToInvalidate + ? markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, + ]); + + return removeResult; + } + + public async update({ + id, + data, + }: UpdateOptions): Promise> { + return await retryIfConflicts( + this.logger, + `alertsClient.update('${id}')`, + async () => await this.updateWithOCC({ id, data }) + ); + } + + private async updateWithOCC({ + id, + data, + }: UpdateOptions): Promise> { + let alertSavedObject: SavedObject; + + try { + alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the object using SOC + alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); + } + + try { + await this.authorization.ensureAuthorized( + alertSavedObject.attributes.alertTypeId, + alertSavedObject.attributes.consumer, + WriteOperations.Update + ); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); + + const updateResult = await this.updateAlert({ id, data }, alertSavedObject); + + await Promise.all([ + alertSavedObject.attributes.apiKey + ? markApiKeyForInvalidation( + { apiKey: alertSavedObject.attributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, + (async () => { + if ( + updateResult.scheduledTaskId && + !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) + ) { + this.taskManager + .runNow(updateResult.scheduledTaskId) + .then(() => { + this.logger.debug( + `Alert update has rescheduled the underlying task: ${updateResult.scheduledTaskId}` + ); + }) + .catch((err: Error) => { + this.logger.error( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` + ); + }); + } + })(), + ]); + + return updateResult; + } + + private async updateAlert( + { id, data }: UpdateOptions, + { attributes, version }: SavedObject + ): Promise> { + const alertType = this.alertTypeRegistry.get(attributes.alertTypeId); + + // Validate + const validatedAlertTypeParams = validateAlertTypeParams( + data.params, + alertType.validate?.params + ); + this.validateActions(alertType, data.actions); + + const { actions, references } = await this.denormalizeActions(data.actions); + const username = await this.getUserName(); + const createdAPIKey = attributes.enabled + ? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name)) + : null; + const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); + const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle); + + let updatedObject: SavedObject; + const createAttributes = this.updateMeta({ + ...attributes, + ...data, + ...apiKeyAttributes, + params: validatedAlertTypeParams as RawAlert['params'], + actions, + notifyWhen, + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + try { + updatedObject = await this.unsecuredSavedObjectsClient.create( + 'alert', + createAttributes, + { + id, + overwrite: true, + version, + references, + } + ); + } catch (e) { + // Avoid unused API key + markApiKeyForInvalidation( + { apiKey: createAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); + throw e; + } + + return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references); + } + + private apiKeyAsAlertAttributes( + apiKey: CreateAPIKeyResult | null, + username: string | null + ): Pick { + return apiKey && apiKey.apiKeysEnabled + ? { + apiKeyOwner: username, + apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), + } + : { + apiKeyOwner: null, + apiKey: null, + }; + } + + public async updateApiKey({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.updateApiKey('${id}')`, + async () => await this.updateApiKeyWithOCC({ id }) + ); + } + + private async updateApiKeyWithOCC({ id }: { id: string }) { + let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; + let version: string | undefined; + + try { + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the attributes and version using SOC + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.UpdateApiKey + ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE_API_KEY, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + const username = await this.getUserName(); + const updateAttributes = this.updateMeta({ + ...attributes, + ...this.apiKeyAsAlertAttributes( + await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), + username + ), + updatedAt: new Date().toISOString(), + updatedBy: username, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE_API_KEY, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + try { + await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); + } catch (e) { + // Avoid unused API key + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); + throw e; + } + + if (apiKeyToInvalidate) { + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); + } + } + + public async enable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.enable('${id}')`, + async () => await this.enableWithOCC({ id }) + ); + } + + private async enableWithOCC({ id }: { id: string }) { + let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; + let version: string | undefined; + + try { + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the attributes and version using SOC + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.Enable + ); + + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.ENABLE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.ENABLE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + if (attributes.enabled === false) { + const username = await this.getUserName(); + const updateAttributes = this.updateMeta({ + ...attributes, + enabled: true, + ...this.apiKeyAsAlertAttributes( + await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), + username + ), + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + try { + await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); + } catch (e) { + // Avoid unused API key + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); + throw e; + } + const scheduledTask = await this.scheduleAlert( + id, + attributes.alertTypeId, + attributes.schedule as IntervalSchedule + ); + await this.unsecuredSavedObjectsClient.update('alert', id, { + scheduledTaskId: scheduledTask.id, + }); + if (apiKeyToInvalidate) { + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); + } + } + } + + public async disable({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.disable('${id}')`, + async () => await this.disableWithOCC({ id }) + ); + } + + private async disableWithOCC({ id }: { id: string }) { + let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; + let version: string | undefined; + + try { + const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + 'alert', + id, + { namespace: this.namespace } + ); + apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + attributes = decryptedAlert.attributes; + version = decryptedAlert.version; + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the attributes and version using SOC + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + attributes = alert.attributes; + version = alert.version; + } + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.Disable + ); + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.DISABLE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.DISABLE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + if (attributes.enabled === true) { + await this.unsecuredSavedObjectsClient.update( + 'alert', + id, + this.updateMeta({ + ...attributes, + enabled: false, + scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }), + { version } + ); + + await Promise.all([ + attributes.scheduledTaskId + ? this.taskManager.removeIfExists(attributes.scheduledTaskId) + : null, + apiKeyToInvalidate + ? await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, + ]); + } + } + + public async muteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteAll('${id}')`, + async () => await this.muteAllWithOCC({ id }) + ); + } + + private async muteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.MuteAll + ); + + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.MUTE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.MUTE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + const updateAttributes = this.updateMeta({ + muteAll: true, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); + } + + public async unmuteAll({ id }: { id: string }): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteAll('${id}')`, + async () => await this.unmuteAllWithOCC({ id }) + ); + } + + private async unmuteAllWithOCC({ id }: { id: string }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.UnmuteAll + ); + + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UNMUTE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UNMUTE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + const updateAttributes = this.updateMeta({ + muteAll: false, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); + } + + public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.muteInstance('${alertId}')`, + async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) + ); + } + + private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.MuteInstance + ); + + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.MUTE_INSTANCE, + savedObject: { type: 'alert', id: alertId }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.MUTE_INSTANCE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id: alertId }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { + mutedInstanceIds.push(alertInstanceId); + await this.unsecuredSavedObjectsClient.update( + 'alert', + alertId, + this.updateMeta({ + mutedInstanceIds, + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }), + { version } + ); + } + } + + public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { + return await retryIfConflicts( + this.logger, + `alertsClient.unmuteInstance('${alertId}')`, + async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) + ); + } + + private async unmuteInstanceWithOCC({ + alertId, + alertInstanceId, + }: { + alertId: string; + alertInstanceId: string; + }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); + + try { + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + WriteOperations.UnmuteInstance + ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UNMUTE_INSTANCE, + savedObject: { type: 'alert', id: alertId }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UNMUTE_INSTANCE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'alert', id: alertId }, + }) + ); + + this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); + + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { + await this.unsecuredSavedObjectsClient.update( + 'alert', + alertId, + this.updateMeta({ + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), + }), + { version } + ); + } + } + + public async listAlertTypes() { + return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [ + ReadOperations.Get, + WriteOperations.Create, + ]); + } + + private async scheduleAlert(id: string, alertTypeId: string, schedule: IntervalSchedule) { + return await this.taskManager.schedule({ + taskType: `alerting:${alertTypeId}`, + schedule, + params: { + alertId: id, + spaceId: this.spaceId, + }, + state: { + previousStartedAt: null, + alertTypeState: {}, + alertInstances: {}, + }, + scope: ['alerting'], + }); + } + + private injectReferencesIntoActions( + alertId: string, + actions: RawAlert['actions'], + references: SavedObjectReference[] + ) { + return actions.map((action) => { + const reference = references.find((ref) => ref.name === action.actionRef); + if (!reference) { + throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); + } + return { + ...omit(action, 'actionRef'), + id: reference.id, + }; + }) as Alert['actions']; + } + + private getAlertFromRaw( + id: string, + rawAlert: RawAlert, + references: SavedObjectReference[] | undefined + ): Alert { + // 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(id, rawAlert, references) as Alert; + } + + private getPartialAlertFromRaw( + id: string, + { createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial, + references: SavedObjectReference[] | undefined + ): PartialAlert { + // Not the prettiest code here, but if we want to use most of the + // alert fields from the rawAlert using `...rawAlert` kind of access, we + // need to specifically delete the executionStatus as it's a different type + // in RawAlert and Alert. Probably next time we need to do something similar + // here, we should look at redesigning the implementation of this method. + const rawAlertWithoutExecutionStatus: Partial> = { + ...rawAlert, + }; + delete rawAlertWithoutExecutionStatus.executionStatus; + const executionStatus = alertExecutionStatusFromRaw(this.logger, id, rawAlert.executionStatus); + return { + id, + notifyWhen, + ...rawAlertWithoutExecutionStatus, + // we currently only support the Interval Schedule type + // Once we support additional types, this type signature will likely change + schedule: rawAlert.schedule as IntervalSchedule, + actions: rawAlert.actions + ? this.injectReferencesIntoActions(id, rawAlert.actions, references || []) + : [], + ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), + ...(createdAt ? { createdAt: new Date(createdAt) } : {}), + ...(scheduledTaskId ? { scheduledTaskId } : {}), + ...(executionStatus ? { executionStatus } : {}), + }; + } + + private validateActions( + alertType: UntypedNormalizedAlertType, + actions: NormalizedAlertAction[] + ): void { + const { actionGroups: alertTypeActionGroups } = alertType; + const usedAlertActionGroups = actions.map((action) => action.group); + const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); + const invalidActionGroups = usedAlertActionGroups.filter( + (group) => !availableAlertTypeActionGroups.has(group) + ); + if (invalidActionGroups.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertsClient.validateActions.invalidGroups', { + defaultMessage: 'Invalid action groups: {groups}', + values: { + groups: invalidActionGroups.join(', '), + }, + }) + ); + } + } + + private async denormalizeActions( + alertActions: NormalizedAlertAction[] + ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { + const references: SavedObjectReference[] = []; + const actions: RawAlert['actions'] = []; + if (alertActions.length) { + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); + const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; + actionTypeIds.forEach((id) => { + // Notify action type usage via "isActionTypeEnabled" function + actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); + }); + alertActions.forEach(({ id, ...alertAction }, i) => { + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + actions.push({ + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }); + } else { + actions.push({ + ...alertAction, + actionRef: '', + actionTypeId: '', + }); + } + }); + } + return { + actions, + references, + }; + } + + private includeFieldsRequiredForAuthentication(fields: string[]): string[] { + return uniq([...fields, 'alertTypeId', 'consumer']); + } + + private generateAPIKeyName(alertTypeId: string, alertName: string) { + return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); + } + + private updateMeta>(alertAttributes: T): T { + if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { + alertAttributes.meta = alertAttributes.meta ?? {}; + alertAttributes.meta.versionApiKeyLastmodified = this.kibanaVersion; + } + return alertAttributes; + } +} + +function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date { + if (dateString === undefined) { + return defaultValue; + } + + const parsedDate = parseIsoOrRelativeDate(dateString); + if (parsedDate === undefined) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertsClient.invalidDate', { + defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"', + values: { + field: propertyName, + dateValue: dateString, + }, + }) + ); + } + + return parsedDate; +} diff --git a/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts b/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts new file mode 100644 index 0000000000000..73bb2a576ace7 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + KibanaRequest, + Logger, + SavedObjectsServiceStart, + PluginInitializerContext, +} from 'src/core/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; +import { RacClient } from './alerts_client'; +import { ALERTS_FEATURE_ID } from '../common'; +import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; +import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../task_manager/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; +import { Space } from '../../spaces/server'; +import { IEventLogClientService } from '../../../plugins/event_log/server'; + +export interface AlertsClientFactoryOpts { + logger: Logger; + taskManager: TaskManagerStartContract; + alertTypeRegistry: AlertTypeRegistry; + securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; + getSpaceId: (request: KibanaRequest) => string | undefined; + getSpace: (request: KibanaRequest) => Promise; + spaceIdToNamespace: SpaceIdToNamespaceFunction; + encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + actions: ActionsPluginStartContract; + features: FeaturesPluginStart; + eventLog: IEventLogClientService; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; +} + +export class AlertsClientFactory { + private isInitialized = false; + private logger!: Logger; + private taskManager!: TaskManagerStartContract; + private alertTypeRegistry!: AlertTypeRegistry; + private securityPluginSetup?: SecurityPluginSetup; + private securityPluginStart?: SecurityPluginStart; + private getSpaceId!: (request: KibanaRequest) => string | undefined; + private getSpace!: (request: KibanaRequest) => Promise; + private spaceIdToNamespace!: SpaceIdToNamespaceFunction; + private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; + private actions!: ActionsPluginStartContract; + private features!: FeaturesPluginStart; + private eventLog!: IEventLogClientService; + private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; + + public initialize(options: RacClientFactoryOpts) { + /** + * This should be called by the plugin's start() method. + */ + if (this.isInitialized) { + throw new Error('AlertsClientFactory already initialized'); + } + this.isInitialized = true; + this.logger = options.logger; + this.getSpaceId = options.getSpaceId; + this.features = options.features; + this.securityPluginSetup = options.securityPluginSetup; + this.securityPluginStart = options.securityPluginStart; + this.spaceIdToNamespace = options.spaceIdToNamespace; + } + + public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): RacClient { + const { features, securityPluginSetup, securityPluginStart } = this; + const spaceId = this.getSpaceId(request); + + const authorization = new AlertsAuthorization({ + authorization: securityPluginStart?.authz, + request, + getSpace: this.getSpace, + features: features!, + }); + + return new RacClient({ + spaceId, + kibanaVersion: this.kibanaVersion, + logger: this.logger, + taskManager: this.taskManager, + alertTypeRegistry: this.alertTypeRegistry, + unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], + }), + authorization, + actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), + namespace: this.spaceIdToNamespace(spaceId), + encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, + auditLogger: securityPluginSetup?.audit.asScoped(request), + async getUserName() { + if (!securityPluginStart) { + return null; + } + const user = await securityPluginStart.authc.getCurrentUser(request); + return user ? user.username : null; + }, + async createAPIKey(name: string) { + if (!securityPluginStart) { + return { apiKeysEnabled: false }; + } + // Create an API key using the new grant API - in this case the Kibana system user is creating the + // API key for the user, instead of having the user create it themselves, which requires api_key + // privileges + const createAPIKeyResult = await securityPluginStart.authc.apiKeys.grantAsInternalUser( + request, + { name, role_descriptors: {} } + ); + if (!createAPIKeyResult) { + return { apiKeysEnabled: false }; + } + return { + apiKeysEnabled: true, + result: createAPIKeyResult, + }; + }, + async getActionsClient() { + return actions.getActionsClientWithRequest(request); + }, + async getEventLogClient() { + return eventLog.getClient(request); + }, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index da355a412207c..fc2163ca1111f 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -93,3 +93,17 @@ export type RuleType< TAdditionalRuleExecutorServices >; }; + +/** + * @public + */ +export interface RacApiRequestHandlerContext { + getRacClient: () => RacClient; +} + +/** + * @internal + */ +export interface RacRequestHandlerContext extends RequestHandlerContext { + rac: RacApiRequestHandlerContext; +} diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 21cf2421ce1b2..9a46fa08208a7 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -10,6 +10,7 @@ import { flatten } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import type { Actions } from '../../actions'; import { FeaturePrivilegeAlertingBuilder } from './alerting'; +import { FeaturePrivilegeAlertsBuilder } from './alerts'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; import { FeaturePrivilegeCatalogueBuilder } from './catalogue'; From f7462aedcf3ea47fdfde2e0616cd2d0e91ef8870 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Thu, 1 Apr 2021 17:16:53 -0400 Subject: [PATCH 2/2] adds rac client initialization to plugin setup / startup and adds scaffolding for CRUD client functions --- x-pack/plugins/rule_registry/server/plugin.ts | 59 +- .../server/rac_client/rac_client.ts | 1604 +++-------------- .../server/rac_client/rac_client_factory.ts | 82 +- x-pack/plugins/rule_registry/server/types.ts | 3 +- 4 files changed, 314 insertions(+), 1434 deletions(-) diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index e533baf89499d..0e601931bd105 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -5,13 +5,27 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup, SharedGlobalConfig } from 'src/core/server'; -import { SecurityPluginSetup } from '../../security/server'; +import { + Logger, + PluginInitializerContext, + Plugin, + CoreSetup, + CoreStart, + SharedGlobalConfig, + KibanaRequest, + IContextProvider, +} from 'src/core/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../alerting/server'; +import { SpacesPluginStart } from '../../spaces/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; + import { RuleRegistry } from './rule_registry'; import { defaultIlmPolicy } from './rule_registry/defaults/ilm_policy'; import { defaultFieldMap } from './rule_registry/defaults/field_map'; -import { RacClientFactory } from './rac_client/rac_client'; +import { RacClientFactory } from './rac_client/rac_client_factory'; +import { RuleRegistryConfig } from '.'; +import { RacRequestHandlerContext } from './types'; export interface RacPluginsSetup { security?: SecurityPluginSetup; alerting: AlertingPluginSetupContract; @@ -19,22 +33,24 @@ export interface RacPluginsSetup { export interface RacPluginsStart { security?: SecurityPluginStart; spaces?: SpacesPluginStart; - alerting: AlertingPluginStartContract; + features: FeaturesPluginStart; } export type RacPluginSetupContract = RuleRegistry; export class RuleRegistryPlugin implements Plugin { - private readonly config: Promise; + private readonly globalConfig: SharedGlobalConfig; + private readonly config: RuleRegistryConfig; private readonly racClientFactory: RacClientFactory; private security?: SecurityPluginSetup; private readonly logger: Logger; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly initContext: PluginInitializerContext) { - this.config = initContext.config.legacy.get(); this.initContext = initContext; this.racClientFactory = new RacClientFactory(); + this.globalConfig = this.initContext.config.legacy.get(); + this.config = initContext.config.get(); this.logger = initContext.logger.get('root'); this.kibanaVersion = initContext.env.packageInfo.version; } @@ -47,25 +63,25 @@ export class RuleRegistryPlugin implements Plugin { core, ilmPolicy: defaultIlmPolicy, fieldMap: defaultFieldMap, - kibanaIndex: this.config.kibana.index, - namespace: 'alert-history', + kibanaIndex: this.globalConfig.kibana.index, + name: 'alert-history', kibanaVersion: this.kibanaVersion, logger: this.logger, alertingPluginSetupContract: plugins.alerting, - writeEnabled: config.writeEnabled, + writeEnabled: this.config.writeEnabled, }); // ALERTS ROUTES core.http.registerRouteHandlerContext( 'rac', - this.createRouteHandlerContext(core) + this.createRouteHandlerContext() ); return rootRegistry; } public start(core: CoreStart, plugins: RacPluginsStart) { - const { logger, licenseState, security, racClientFactory } = this; + const { logger, security, racClientFactory } = this; racClientFactory.initialize({ logger, @@ -82,12 +98,7 @@ export class RuleRegistryPlugin implements Plugin { }); const getRacClientWithRequest = (request: KibanaRequest) => { - if (isESOCanEncrypt !== true) { - throw new Error( - `Unable to create rac client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` - ); - } - return racClientFactory!.create(request, core.savedObjects); + return racClientFactory!.create(request); }; return { @@ -95,19 +106,13 @@ export class RuleRegistryPlugin implements Plugin { }; } - private createRouteHandlerContext = ( - core: CoreSetup - ): IContextProvider => { - const { alertTypeRegistry, alertsClientFactory } = this; + private createRouteHandlerContext = (): IContextProvider => { + const { racClientFactory } = this; return async function alertsRouteHandlerContext(context, request) { - const [{ savedObjects }] = await core.getStartServices(); return { - getAlertsClient: () => { - return alertsClientFactory!.create(request, savedObjects); + getRacClient: () => { + return racClientFactory!.create(request); }, - listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!), - getFrameworkHealth: async () => - await getHealth(savedObjects.createInternalRepository(['alert'])), }; }; }; diff --git a/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts b/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts index 7bfb6bd5dde02..9677cd9148b78 100644 --- a/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts @@ -5,20 +5,15 @@ * 2.0. */ -import Boom from '@hapi/boom'; -import { omit, isEqual, map, uniq, pick, truncate, trim } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { isEqual, pick } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { Logger, - SavedObjectsClientContract, - SavedObjectReference, SavedObject, PluginInitializerContext, SavedObjectsUtils, } from '../../../../../src/core/server'; import { esKuery } from '../../../../../src/plugins/data/server'; -import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { Alert, PartialAlert, @@ -42,23 +37,12 @@ import { GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../security/server'; -import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../../task_manager/server'; -import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; -import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry'; + +// TODO: implement the authorization class import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization'; -import { IEventLogClient } from '../../../../plugins/event_log/server'; -import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; -import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; -import { IEvent } from '../../../event_log/server'; import { AuditLogger, EventOutcome } from '../../../security/server'; -import { parseDuration } from '../../common/parse_duration'; -import { retryIfConflicts } from '../lib/retry_if_conflicts'; -import { partiallyUpdateAlert } from '../saved_objects'; -import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; import { alertAuditEvent, AlertAuditAction } from './audit_events'; import { nodeBuilder } from '../../../../../src/plugins/data/common'; -import { mapSortField } from './lib'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -73,27 +57,12 @@ export type InvalidateAPIKeyResult = export interface ConstructorOptions { logger: Logger; - taskManager: TaskManagerStartContract; - unsecuredSavedObjectsClient: SavedObjectsClientContract; authorization: AlertsAuthorization; - actionsAuthorization: ActionsAuthorization; - alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; - namespace?: string; - getUserName: () => Promise; - createAPIKey: (name: string) => Promise; - getActionsClient: () => Promise; - getEventLogClient: () => Promise; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; auditLogger?: AuditLogger; } -export interface MuteOptions extends IndexType { - alertId: string; - alertInstanceId: string; -} - export interface FindOptions extends IndexType { perPage?: number; page?: number; @@ -110,17 +79,6 @@ export interface FindOptions extends IndexType { filter?: string; } -export interface AggregateOptions extends IndexType { - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - hasReference?: { - type: string; - id: string; - }; - filter?: string; -} - interface IndexType { [key: string]: unknown; } @@ -177,51 +135,16 @@ export interface GetAlertInstanceSummaryParams { export class RacClient { private readonly logger: Logger; - private readonly getUserName: () => Promise; private readonly spaceId?: string; - private readonly namespace?: string; - private readonly taskManager: TaskManagerStartContract; - private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; private readonly authorization: AlertsAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; - private readonly createAPIKey: (name: string) => Promise; - private readonly getActionsClient: () => Promise; - private readonly actionsAuthorization: ActionsAuthorization; - private readonly getEventLogClient: () => Promise; - private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; private readonly auditLogger?: AuditLogger; - constructor({ - alertTypeRegistry, - unsecuredSavedObjectsClient, - authorization, - taskManager, - logger, - spaceId, - namespace, - getUserName, - createAPIKey, - encryptedSavedObjectsClient, - getActionsClient, - actionsAuthorization, - getEventLogClient, - kibanaVersion, - auditLogger, - }: ConstructorOptions) { + constructor({ authorization, logger, spaceId, kibanaVersion, auditLogger }: ConstructorOptions) { this.logger = logger; - this.getUserName = getUserName; this.spaceId = spaceId; - this.namespace = namespace; - this.taskManager = taskManager; - this.alertTypeRegistry = alertTypeRegistry; - this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.authorization = authorization; - this.createAPIKey = createAPIKey; - this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; - this.getActionsClient = getActionsClient; - this.actionsAuthorization = actionsAuthorization; - this.getEventLogClient = getEventLogClient; this.kibanaVersion = kibanaVersion; this.auditLogger = auditLogger; } @@ -230,120 +153,109 @@ export class RacClient { data, options, }: CreateOptions): Promise> { - const id = options?.id || SavedObjectsUtils.generateId(); - - try { - await this.authorization.ensureAuthorized( - data.alertTypeId, - data.consumer, - WriteOperations.Create - ); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId); - - // Throws an error if alert type isn't registered - const alertType = this.alertTypeRegistry.get(data.alertTypeId); - - const validatedAlertTypeParams = validateAlertTypeParams( - data.params, - alertType.validate?.params - ); - const username = await this.getUserName(); - - const createdAPIKey = data.enabled - ? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name)) - : null; - - this.validateActions(alertType, data.actions); - - const createTime = Date.now(); - const { references, actions } = await this.denormalizeActions(data.actions); - - const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle); - - const rawAlert: RawAlert = { - ...data, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), - actions, - createdBy: username, - updatedBy: username, - createdAt: new Date(createTime).toISOString(), - updatedAt: new Date(createTime).toISOString(), - params: validatedAlertTypeParams as RawAlert['params'], - muteAll: false, - mutedInstanceIds: [], - notifyWhen, - executionStatus: { - status: 'pending', - lastExecutionDate: new Date().toISOString(), - error: null, - }, - }; - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - let createdAlert: SavedObject; - try { - createdAlert = await this.unsecuredSavedObjectsClient.create( - 'alert', - this.updateMeta(rawAlert), - { - ...options, - references, - id, - } - ); - } catch (e) { - // Avoid unused API key - markApiKeyForInvalidation( - { apiKey: rawAlert.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - if (data.enabled) { - let scheduledTask; - try { - scheduledTask = await this.scheduleAlert( - createdAlert.id, - rawAlert.alertTypeId, - data.schedule - ); - } catch (e) { - // Cleanup data, something went wrong scheduling the task - try { - await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); - } catch (err) { - // Skip the cleanup error and throw the task manager error to avoid confusion - this.logger.error( - `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}` - ); - } - throw e; - } - await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { - scheduledTaskId: scheduledTask.id, - }); - createdAlert.attributes.scheduledTaskId = scheduledTask.id; - } - return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); + // const id = options?.id || SavedObjectsUtils.generateId(); + // try { + // await this.authorization.ensureAuthorized( + // data.alertTypeId, + // data.consumer, + // WriteOperations.Create + // ); + // } catch (error) { + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.CREATE, + // savedObject: { type: 'alert', id }, + // error, + // }) + // ); + // throw error; + // } + // this.alertTypeRegistry.ensureAlertTypeEnabled(data.alertTypeId); + // // Throws an error if alert type isn't registered + // const alertType = this.alertTypeRegistry.get(data.alertTypeId); + // const validatedAlertTypeParams = validateAlertTypeParams( + // data.params, + // alertType.validate?.params + // ); + // const username = await this.getUserName(); + // const createdAPIKey = data.enabled + // ? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name)) + // : null; + // this.validateActions(alertType, data.actions); + // const createTime = Date.now(); + // const { references, actions } = await this.denormalizeActions(data.actions); + // const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle); + // const rawAlert: RawAlert = { + // ...data, + // ...this.apiKeyAsAlertAttributes(createdAPIKey, username), + // actions, + // createdBy: username, + // updatedBy: username, + // createdAt: new Date(createTime).toISOString(), + // updatedAt: new Date(createTime).toISOString(), + // params: validatedAlertTypeParams as RawAlert['params'], + // muteAll: false, + // mutedInstanceIds: [], + // notifyWhen, + // executionStatus: { + // status: 'pending', + // lastExecutionDate: new Date().toISOString(), + // error: null, + // }, + // }; + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.CREATE, + // outcome: EventOutcome.UNKNOWN, + // savedObject: { type: 'alert', id }, + // }) + // ); + // let createdAlert: SavedObject; + // try { + // createdAlert = await this.unsecuredSavedObjectsClient.create( + // 'alert', + // this.updateMeta(rawAlert), + // { + // ...options, + // references, + // id, + // } + // ); + // } catch (e) { + // // Avoid unused API key + // markApiKeyForInvalidation( + // { apiKey: rawAlert.apiKey }, + // this.logger, + // this.unsecuredSavedObjectsClient + // ); + // throw e; + // } + // if (data.enabled) { + // let scheduledTask; + // try { + // scheduledTask = await this.scheduleAlert( + // createdAlert.id, + // rawAlert.alertTypeId, + // data.schedule + // ); + // } catch (e) { + // // Cleanup data, something went wrong scheduling the task + // try { + // await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); + // } catch (err) { + // // Skip the cleanup error and throw the task manager error to avoid confusion + // this.logger.error( + // `Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}` + // ); + // } + // throw e; + // } + // await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + // scheduledTaskId: scheduledTask.id, + // }); + // createdAlert.attributes.scheduledTaskId = scheduledTask.id; + // } + // return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); } public async get({ @@ -351,1160 +263,186 @@ export class RacClient { }: { id: string; }): Promise> { - const result = await this.unsecuredSavedObjectsClient.get('alert', id); - try { - await this.authorization.ensureAuthorized( - result.attributes.alertTypeId, - result.attributes.consumer, - ReadOperations.Get - ); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, - savedObject: { type: 'alert', id }, - }) - ); - return this.getAlertFromRaw(result.id, result.attributes, result.references); - } - - public async getAlertState({ id }: { id: string }): Promise { - const alert = await this.get({ id }); - await this.authorization.ensureAuthorized( - alert.alertTypeId, - alert.consumer, - ReadOperations.GetAlertState - ); - if (alert.scheduledTaskId) { - const { state } = taskInstanceToAlertTaskInstance( - await this.taskManager.get(alert.scheduledTaskId), - alert - ); - return state; - } - } - - public async getAlertInstanceSummary({ - id, - dateStart, - }: GetAlertInstanceSummaryParams): Promise { - this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); - const alert = await this.get({ id }); - await this.authorization.ensureAuthorized( - alert.alertTypeId, - alert.consumer, - ReadOperations.GetAlertInstanceSummary - ); - - // default duration of instance summary is 60 * alert interval - const dateNow = new Date(); - const durationMillis = parseDuration(alert.schedule.interval) * 60; - const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); - const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); - - const eventLogClient = await this.getEventLogClient(); - - this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); - let events: IEvent[]; - try { - const queryResults = await eventLogClient.findEventsBySavedObjectIds('alert', [id], { - page: 1, - per_page: 10000, - start: parsedDateStart.toISOString(), - end: dateNow.toISOString(), - sort_order: 'desc', - }); - events = queryResults.data; - } catch (err) { - this.logger.debug( - `alertsClient.getAlertInstanceSummary(): error searching event log for alert ${id}: ${err.message}` - ); - events = []; - } - - return alertInstanceSummaryFromEventLog({ - alert, - events, - dateStart: parsedDateStart.toISOString(), - dateEnd: dateNow.toISOString(), - }); + // const result = await this.unsecuredSavedObjectsClient.get('alert', id); + // try { + // await this.authorization.ensureAuthorized( + // result.attributes.alertTypeId, + // result.attributes.consumer, + // ReadOperations.Get + // ); + // } catch (error) { + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.GET, + // savedObject: { type: 'alert', id }, + // error, + // }) + // ); + // throw error; + // } + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.GET, + // savedObject: { type: 'alert', id }, + // }) + // ); + // return this.getAlertFromRaw(result.id, result.attributes, result.references); } public async find({ options: { fields, ...options } = {}, }: { options?: FindOptions } = {}): Promise> { - let authorizationTuple; - try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter(); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, - error, - }) - ); - throw error; - } - const { - filter: authorizationFilter, - ensureAlertTypeIsAuthorized, - logSuccessfulAuthorization, - } = authorizationTuple; - - const { - page, - per_page: perPage, - total, - saved_objects: data, - } = await this.unsecuredSavedObjectsClient.find({ - ...options, - sortField: mapSortField(options.sortField), - filter: - (authorizationFilter && options.filter - ? nodeBuilder.and([esKuery.fromKueryExpression(options.filter), authorizationFilter]) - : authorizationFilter) ?? options.filter, - fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, - type: 'alert', - }); - - const authorizedData = data.map(({ id, attributes, references }) => { - try { - ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - return this.getAlertFromRaw( - id, - fields ? (pick(attributes, fields) as RawAlert) : attributes, - references - ); - }); - - authorizedData.forEach(({ id }) => - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, - savedObject: { type: 'alert', id }, - }) - ) - ); - - logSuccessfulAuthorization(); - - return { - page, - perPage, - total, - data: authorizedData, - }; - } - - public async aggregate({ - options: { fields, ...options } = {}, - }: { options?: AggregateOptions } = {}): Promise { - // Replace this when saved objects supports aggregations https://github.com/elastic/kibana/pull/64002 - const alertExecutionStatus = await Promise.all( - AlertExecutionStatusValues.map(async (status: string) => { - const { - filter: authorizationFilter, - logSuccessfulAuthorization, - } = await this.authorization.getFindAuthorizationFilter(); - const filter = options.filter - ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` - : `alert.attributes.executionStatus.status:(${status})`; - const { total } = await this.unsecuredSavedObjectsClient.find({ - ...options, - filter: - (authorizationFilter && filter - ? nodeBuilder.and([esKuery.fromKueryExpression(filter), authorizationFilter]) - : authorizationFilter) ?? filter, - page: 1, - perPage: 0, - type: 'alert', - }); - - logSuccessfulAuthorization(); - - return { [status]: total }; - }) - ); - - return { - alertExecutionStatus: alertExecutionStatus.reduce( - (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), - {} - ), - }; - } - - public async delete({ id }: { id: string }) { - let taskIdToRemove: string | undefined | null; - let apiKeyToInvalidate: string | null = null; - let attributes: RawAlert; - - try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { namespace: this.namespace } - ); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; - attributes = decryptedAlert.attributes; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the scheduledTaskId using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - taskIdToRemove = alert.attributes.scheduledTaskId; - attributes = alert.attributes; - } - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Delete - ); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); - - await Promise.all([ - taskIdToRemove ? this.taskManager.removeIfExists(taskIdToRemove) : null, - apiKeyToInvalidate - ? markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - ]); - - return removeResult; + // let authorizationTuple; + // try { + // authorizationTuple = await this.authorization.getFindAuthorizationFilter(); + // } catch (error) { + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.FIND, + // error, + // }) + // ); + // throw error; + // } + // const { + // filter: authorizationFilter, + // ensureAlertTypeIsAuthorized, + // logSuccessfulAuthorization, + // } = authorizationTuple; + // const { + // page, + // per_page: perPage, + // total, + // saved_objects: data, + // } = await this.unsecuredSavedObjectsClient.find({ + // ...options, + // sortField: mapSortField(options.sortField), + // filter: + // (authorizationFilter && options.filter + // ? nodeBuilder.and([esKuery.fromKueryExpression(options.filter), authorizationFilter]) + // : authorizationFilter) ?? options.filter, + // fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, + // type: 'alert', + // }); + // const authorizedData = data.map(({ id, attributes, references }) => { + // try { + // ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); + // } catch (error) { + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.FIND, + // savedObject: { type: 'alert', id }, + // error, + // }) + // ); + // throw error; + // } + // return this.getAlertFromRaw( + // id, + // fields ? (pick(attributes, fields) as RawAlert) : attributes, + // references + // ); + // }); + // authorizedData.forEach(({ id }) => + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.FIND, + // savedObject: { type: 'alert', id }, + // }) + // ) + // ); + // logSuccessfulAuthorization(); + // return { + // page, + // perPage, + // total, + // data: authorizedData, + // }; } public async update({ id, data, }: UpdateOptions): Promise> { - return await retryIfConflicts( - this.logger, - `alertsClient.update('${id}')`, - async () => await this.updateWithOCC({ id, data }) - ); + // return await retryIfConflicts( + // this.logger, + // `alertsClient.update('${id}')`, + // async () => await this.updateWithOCC({ id, data }) + // ); } private async updateWithOCC({ id, data, }: UpdateOptions): Promise> { - let alertSavedObject: SavedObject; - - try { - alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { namespace: this.namespace } - ); - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the object using SOC - alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); - } - - try { - await this.authorization.ensureAuthorized( - alertSavedObject.attributes.alertTypeId, - alertSavedObject.attributes.consumer, - WriteOperations.Update - ); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); - - const updateResult = await this.updateAlert({ id, data }, alertSavedObject); - - await Promise.all([ - alertSavedObject.attributes.apiKey - ? markApiKeyForInvalidation( - { apiKey: alertSavedObject.attributes.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - (async () => { - if ( - updateResult.scheduledTaskId && - !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) - ) { - this.taskManager - .runNow(updateResult.scheduledTaskId) - .then(() => { - this.logger.debug( - `Alert update has rescheduled the underlying task: ${updateResult.scheduledTaskId}` - ); - }) - .catch((err: Error) => { - this.logger.error( - `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` - ); - }); - } - })(), - ]); - - return updateResult; - } - - private async updateAlert( - { id, data }: UpdateOptions, - { attributes, version }: SavedObject - ): Promise> { - const alertType = this.alertTypeRegistry.get(attributes.alertTypeId); - - // Validate - const validatedAlertTypeParams = validateAlertTypeParams( - data.params, - alertType.validate?.params - ); - this.validateActions(alertType, data.actions); - - const { actions, references } = await this.denormalizeActions(data.actions); - const username = await this.getUserName(); - const createdAPIKey = attributes.enabled - ? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name)) - : null; - const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle); - - let updatedObject: SavedObject; - const createAttributes = this.updateMeta({ - ...attributes, - ...data, - ...apiKeyAttributes, - params: validatedAlertTypeParams as RawAlert['params'], - actions, - notifyWhen, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - try { - updatedObject = await this.unsecuredSavedObjectsClient.create( - 'alert', - createAttributes, - { - id, - overwrite: true, - version, - references, - } - ); - } catch (e) { - // Avoid unused API key - markApiKeyForInvalidation( - { apiKey: createAttributes.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - - return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references); - } - - private apiKeyAsAlertAttributes( - apiKey: CreateAPIKeyResult | null, - username: string | null - ): Pick { - return apiKey && apiKey.apiKeysEnabled - ? { - apiKeyOwner: username, - apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), - } - : { - apiKeyOwner: null, - apiKey: null, - }; - } - - public async updateApiKey({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.updateApiKey('${id}')`, - async () => await this.updateApiKeyWithOCC({ id }) - ); - } - - private async updateApiKeyWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; - let attributes: RawAlert; - let version: string | undefined; - - try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { namespace: this.namespace } - ); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UpdateApiKey - ); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - const username = await this.getUserName(); - const updateAttributes = this.updateMeta({ - ...attributes, - ...this.apiKeyAsAlertAttributes( - await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), - username - ), - updatedAt: new Date().toISOString(), - updatedBy: username, - }); - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - try { - await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); - } catch (e) { - // Avoid unused API key - markApiKeyForInvalidation( - { apiKey: updateAttributes.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - - if (apiKeyToInvalidate) { - await markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } - } - - public async enable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.enable('${id}')`, - async () => await this.enableWithOCC({ id }) - ); - } - - private async enableWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; - let attributes: RawAlert; - let version: string | undefined; - - try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { namespace: this.namespace } - ); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Enable - ); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === false) { - const username = await this.getUserName(); - const updateAttributes = this.updateMeta({ - ...attributes, - enabled: true, - ...this.apiKeyAsAlertAttributes( - await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), - username - ), - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - try { - await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); - } catch (e) { - // Avoid unused API key - markApiKeyForInvalidation( - { apiKey: updateAttributes.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ); - throw e; - } - const scheduledTask = await this.scheduleAlert( - id, - attributes.alertTypeId, - attributes.schedule as IntervalSchedule - ); - await this.unsecuredSavedObjectsClient.update('alert', id, { - scheduledTaskId: scheduledTask.id, - }); - if (apiKeyToInvalidate) { - await markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } - } - } - - public async disable({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.disable('${id}')`, - async () => await this.disableWithOCC({ id }) - ); + // let alertSavedObject: SavedObject; + // try { + // alertSavedObject = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + // 'alert', + // id, + // { namespace: this.namespace } + // ); + // } catch (e) { + // // We'll skip invalidating the API key since we failed to load the decrypted saved object + // this.logger.error( + // `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + // ); + // // Still attempt to load the object using SOC + // alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); + // } + // try { + // await this.authorization.ensureAuthorized( + // alertSavedObject.attributes.alertTypeId, + // alertSavedObject.attributes.consumer, + // WriteOperations.Update + // ); + // } catch (error) { + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.UPDATE, + // savedObject: { type: 'alert', id }, + // error, + // }) + // ); + // throw error; + // } + // this.auditLogger?.log( + // alertAuditEvent({ + // action: AlertAuditAction.UPDATE, + // outcome: EventOutcome.UNKNOWN, + // savedObject: { type: 'alert', id }, + // }) + // ); + // this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId); + // const updateResult = await this.updateAlert({ id, data }, alertSavedObject); + // await Promise.all([ + // alertSavedObject.attributes.apiKey + // ? markApiKeyForInvalidation( + // { apiKey: alertSavedObject.attributes.apiKey }, + // this.logger, + // this.unsecuredSavedObjectsClient + // ) + // : null, + // (async () => { + // if ( + // updateResult.scheduledTaskId && + // !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) + // ) { + // this.taskManager + // .runNow(updateResult.scheduledTaskId) + // .then(() => { + // this.logger.debug( + // `Alert update has rescheduled the underlying task: ${updateResult.scheduledTaskId}` + // ); + // }) + // .catch((err: Error) => { + // this.logger.error( + // `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` + // ); + // }); + // } + // })(), + // ]); + // return updateResult; } - - private async disableWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; - let attributes: RawAlert; - let version: string | undefined; - - try { - const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'alert', - id, - { namespace: this.namespace } - ); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; - attributes = decryptedAlert.attributes; - version = decryptedAlert.version; - } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); - // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); - attributes = alert.attributes; - version = alert.version; - } - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Disable - ); - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - if (attributes.enabled === true) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - id, - this.updateMeta({ - ...attributes, - enabled: false, - scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }), - { version } - ); - - await Promise.all([ - attributes.scheduledTaskId - ? this.taskManager.removeIfExists(attributes.scheduledTaskId) - : null, - apiKeyToInvalidate - ? await markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - ]); - } - } - - public async muteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.muteAll('${id}')`, - async () => await this.muteAllWithOCC({ id }) - ); - } - - private async muteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.MuteAll - ); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: true, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async unmuteAll({ id }: { id: string }): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.unmuteAll('${id}')`, - async () => await this.unmuteAllWithOCC({ id }) - ); - } - - private async unmuteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UnmuteAll - ); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, - savedObject: { type: 'alert', id }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - const updateAttributes = this.updateMeta({ - muteAll: false, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }); - const updateOptions = { version }; - - await partiallyUpdateAlert( - this.unsecuredSavedObjectsClient, - id, - updateAttributes, - updateOptions - ); - } - - public async muteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.muteInstance('${alertId}')`, - async () => await this.muteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.MuteInstance - ); - - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { - mutedInstanceIds.push(alertInstanceId); - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - mutedInstanceIds, - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - }), - { version } - ); - } - } - - public async unmuteInstance({ alertId, alertInstanceId }: MuteOptions): Promise { - return await retryIfConflicts( - this.logger, - `alertsClient.unmuteInstance('${alertId}')`, - async () => await this.unmuteInstanceWithOCC({ alertId, alertInstanceId }) - ); - } - - private async unmuteInstanceWithOCC({ - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); - - try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UnmuteInstance - ); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - } catch (error) { - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, - savedObject: { type: 'alert', id: alertId }, - error, - }) - ); - throw error; - } - - this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, - outcome: EventOutcome.UNKNOWN, - savedObject: { type: 'alert', id: alertId }, - }) - ); - - this.alertTypeRegistry.ensureAlertTypeEnabled(attributes.alertTypeId); - - const mutedInstanceIds = attributes.mutedInstanceIds || []; - if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( - 'alert', - alertId, - this.updateMeta({ - updatedBy: await this.getUserName(), - updatedAt: new Date().toISOString(), - mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), - }), - { version } - ); - } - } - - public async listAlertTypes() { - return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [ - ReadOperations.Get, - WriteOperations.Create, - ]); - } - - private async scheduleAlert(id: string, alertTypeId: string, schedule: IntervalSchedule) { - return await this.taskManager.schedule({ - taskType: `alerting:${alertTypeId}`, - schedule, - params: { - alertId: id, - spaceId: this.spaceId, - }, - state: { - previousStartedAt: null, - alertTypeState: {}, - alertInstances: {}, - }, - scope: ['alerting'], - }); - } - - private injectReferencesIntoActions( - alertId: string, - actions: RawAlert['actions'], - references: SavedObjectReference[] - ) { - return actions.map((action) => { - const reference = references.find((ref) => ref.name === action.actionRef); - if (!reference) { - throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); - } - return { - ...omit(action, 'actionRef'), - id: reference.id, - }; - }) as Alert['actions']; - } - - private getAlertFromRaw( - id: string, - rawAlert: RawAlert, - references: SavedObjectReference[] | undefined - ): Alert { - // 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(id, rawAlert, references) as Alert; - } - - private getPartialAlertFromRaw( - id: string, - { createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial, - references: SavedObjectReference[] | undefined - ): PartialAlert { - // Not the prettiest code here, but if we want to use most of the - // alert fields from the rawAlert using `...rawAlert` kind of access, we - // need to specifically delete the executionStatus as it's a different type - // in RawAlert and Alert. Probably next time we need to do something similar - // here, we should look at redesigning the implementation of this method. - const rawAlertWithoutExecutionStatus: Partial> = { - ...rawAlert, - }; - delete rawAlertWithoutExecutionStatus.executionStatus; - const executionStatus = alertExecutionStatusFromRaw(this.logger, id, rawAlert.executionStatus); - return { - id, - notifyWhen, - ...rawAlertWithoutExecutionStatus, - // we currently only support the Interval Schedule type - // Once we support additional types, this type signature will likely change - schedule: rawAlert.schedule as IntervalSchedule, - actions: rawAlert.actions - ? this.injectReferencesIntoActions(id, rawAlert.actions, references || []) - : [], - ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), - ...(createdAt ? { createdAt: new Date(createdAt) } : {}), - ...(scheduledTaskId ? { scheduledTaskId } : {}), - ...(executionStatus ? { executionStatus } : {}), - }; - } - - private validateActions( - alertType: UntypedNormalizedAlertType, - actions: NormalizedAlertAction[] - ): void { - const { actionGroups: alertTypeActionGroups } = alertType; - const usedAlertActionGroups = actions.map((action) => action.group); - const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); - const invalidActionGroups = usedAlertActionGroups.filter( - (group) => !availableAlertTypeActionGroups.has(group) - ); - if (invalidActionGroups.length) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.alertsClient.validateActions.invalidGroups', { - defaultMessage: 'Invalid action groups: {groups}', - values: { - groups: invalidActionGroups.join(', '), - }, - }) - ); - } - } - - private async denormalizeActions( - alertActions: NormalizedAlertAction[] - ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const references: SavedObjectReference[] = []; - const actions: RawAlert['actions'] = []; - if (alertActions.length) { - const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; - const actionResults = await actionsClient.getBulk(actionIds); - const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; - actionTypeIds.forEach((id) => { - // Notify action type usage via "isActionTypeEnabled" function - actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); - }); - alertActions.forEach(({ id, ...alertAction }, i) => { - const actionResultValue = actionResults.find((action) => action.id === id); - if (actionResultValue) { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - actions.push({ - ...alertAction, - actionRef, - actionTypeId: actionResultValue.actionTypeId, - }); - } else { - actions.push({ - ...alertAction, - actionRef: '', - actionTypeId: '', - }); - } - }); - } - return { - actions, - references, - }; - } - - private includeFieldsRequiredForAuthentication(fields: string[]): string[] { - return uniq([...fields, 'alertTypeId', 'consumer']); - } - - private generateAPIKeyName(alertTypeId: string, alertName: string) { - return truncate(`Alerting: ${alertTypeId}/${trim(alertName)}`, { length: 256 }); - } - - private updateMeta>(alertAttributes: T): T { - if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { - alertAttributes.meta = alertAttributes.meta ?? {}; - alertAttributes.meta.versionApiKeyLastmodified = this.kibanaVersion; - } - return alertAttributes; - } -} - -function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date { - if (dateString === undefined) { - return defaultValue; - } - - const parsedDate = parseIsoOrRelativeDate(dateString); - if (parsedDate === undefined) { - throw Boom.badRequest( - i18n.translate('xpack.alerting.alertsClient.invalidDate', { - defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"', - values: { - field: propertyName, - dateValue: dateString, - }, - }) - ); - } - - return parsedDate; } diff --git a/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts b/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts index 73bb2a576ace7..6f04d387e4815 100644 --- a/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts @@ -5,55 +5,33 @@ * 2.0. */ -import { - KibanaRequest, - Logger, - SavedObjectsServiceStart, - PluginInitializerContext, -} from 'src/core/server'; -import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; -import { RacClient } from './alerts_client'; -import { ALERTS_FEATURE_ID } from '../common'; -import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; -import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../task_manager/server'; -import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { KibanaRequest, Logger, PluginInitializerContext } from 'src/core/server'; +import { RacClient } from './rac_client'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +// TODO: implement this class and audit logger import { AlertsAuthorization } from './authorization/alerts_authorization'; import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; -import { Space } from '../../spaces/server'; -import { IEventLogClientService } from '../../../plugins/event_log/server'; +import { Space } from '../../../spaces/server'; -export interface AlertsClientFactoryOpts { +export interface RacClientFactoryOpts { logger: Logger; - taskManager: TaskManagerStartContract; - alertTypeRegistry: AlertTypeRegistry; securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpaceId: (request: KibanaRequest) => string | undefined; getSpace: (request: KibanaRequest) => Promise; - spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - actions: ActionsPluginStartContract; features: FeaturesPluginStart; - eventLog: IEventLogClientService; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } -export class AlertsClientFactory { +export class RacClientFactory { private isInitialized = false; private logger!: Logger; - private taskManager!: TaskManagerStartContract; - private alertTypeRegistry!: AlertTypeRegistry; private securityPluginSetup?: SecurityPluginSetup; private securityPluginStart?: SecurityPluginStart; private getSpaceId!: (request: KibanaRequest) => string | undefined; private getSpace!: (request: KibanaRequest) => Promise; - private spaceIdToNamespace!: SpaceIdToNamespaceFunction; - private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; - private actions!: ActionsPluginStartContract; private features!: FeaturesPluginStart; - private eventLog!: IEventLogClientService; private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; public initialize(options: RacClientFactoryOpts) { @@ -69,10 +47,9 @@ export class AlertsClientFactory { this.features = options.features; this.securityPluginSetup = options.securityPluginSetup; this.securityPluginStart = options.securityPluginStart; - this.spaceIdToNamespace = options.spaceIdToNamespace; } - public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): RacClient { + public create(request: KibanaRequest): RacClient { const { features, securityPluginSetup, securityPluginStart } = this; const spaceId = this.getSpaceId(request); @@ -87,49 +64,8 @@ export class AlertsClientFactory { spaceId, kibanaVersion: this.kibanaVersion, logger: this.logger, - taskManager: this.taskManager, - alertTypeRegistry: this.alertTypeRegistry, - unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { - excludedWrappers: ['security'], - includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], - }), authorization, - actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), - namespace: this.spaceIdToNamespace(spaceId), - encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, auditLogger: securityPluginSetup?.audit.asScoped(request), - async getUserName() { - if (!securityPluginStart) { - return null; - } - const user = await securityPluginStart.authc.getCurrentUser(request); - return user ? user.username : null; - }, - async createAPIKey(name: string) { - if (!securityPluginStart) { - return { apiKeysEnabled: false }; - } - // Create an API key using the new grant API - in this case the Kibana system user is creating the - // API key for the user, instead of having the user create it themselves, which requires api_key - // privileges - const createAPIKeyResult = await securityPluginStart.authc.apiKeys.grantAsInternalUser( - request, - { name, role_descriptors: {} } - ); - if (!createAPIKeyResult) { - return { apiKeysEnabled: false }; - } - return { - apiKeysEnabled: true, - result: createAPIKeyResult, - }; - }, - async getActionsClient() { - return actions.getActionsClientWithRequest(request); - }, - async getEventLogClient() { - return eventLog.getClient(request); - }, }); } } diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index fc2163ca1111f..3dacb35f64429 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ import { Type, TypeOf } from '@kbn/config-schema'; -import { Logger } from 'kibana/server'; +import { Logger, RequestHandlerContext } from 'kibana/server'; import { ActionVariable, AlertInstanceContext, @@ -14,6 +14,7 @@ import { AlertTypeState, } from '../../alerting/common'; import { ActionGroup, AlertExecutorOptions } from '../../alerting/server'; +import { RacClient } from './rac_client/rac_client'; import { ScopedRuleRegistryClient } from './rule_registry/create_scoped_rule_registry_client/types'; import { DefaultFieldMap } from './rule_registry/defaults/field_map';