diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 17fe2b20f74fa8e..eca6a93ae880637 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 9e83d938d508b84..f2bcc57296b8ffe 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -5,29 +5,60 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/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_factory'; import { RuleRegistryConfig } from '.'; +import { RacRequestHandlerContext } from './types'; +export interface RacPluginsSetup { + security?: SecurityPluginSetup; + alerting: AlertingPluginSetupContract; +} +export interface RacPluginsStart { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; + features: FeaturesPluginStart; +} -export type RuleRegistryPluginSetupContract = RuleRegistry; +export type RacPluginSetupContract = RuleRegistry; + +export class RuleRegistryPlugin implements Plugin { + 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']; -export class RuleRegistryPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { 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; } - 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({ coreSetup: core, ilmPolicy: defaultIlmPolicy, @@ -37,13 +68,54 @@ export class RuleRegistryPlugin implements Plugin( + 'rac', + this.createRouteHandlerContext() + ); + return rootRegistry; } - public start() {} + public start(core: CoreStart, plugins: RacPluginsStart) { + const { logger, 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) => { + return racClientFactory!.create(request); + }; + + return { + getRacClientWithRequest, + }; + } + + private createRouteHandlerContext = (): IContextProvider => { + const { racClientFactory } = this; + return async function alertsRouteHandlerContext(context, request) { + return { + getRacClient: () => { + return racClientFactory!.create(request); + }, + }; + }; + }; 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 000000000000000..9677cd9148b78e8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client.ts @@ -0,0 +1,448 @@ +/* + * 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 { isEqual, pick } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; +import { + Logger, + SavedObject, + PluginInitializerContext, + SavedObjectsUtils, +} from '../../../../../src/core/server'; +import { esKuery } from '../../../../../src/plugins/data/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'; + +// TODO: implement the authorization class +import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization'; +import { AuditLogger, EventOutcome } from '../../../security/server'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; + +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; + authorization: AlertsAuthorization; + spaceId?: string; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + auditLogger?: AuditLogger; +} + +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; +} + +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 spaceId?: string; + private readonly authorization: AlertsAuthorization; + private readonly alertTypeRegistry: AlertTypeRegistry; + private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; + private readonly auditLogger?: AuditLogger; + + constructor({ authorization, logger, spaceId, kibanaVersion, auditLogger }: ConstructorOptions) { + this.logger = logger; + this.spaceId = spaceId; + this.authorization = authorization; + 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 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 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; + } +} 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 000000000000000..6f04d387e481538 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rac_client/rac_client_factory.ts @@ -0,0 +1,71 @@ +/* + * 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, 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'; + +export interface RacClientFactoryOpts { + logger: Logger; + securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; + getSpaceId: (request: KibanaRequest) => string | undefined; + getSpace: (request: KibanaRequest) => Promise; + features: FeaturesPluginStart; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; +} + +export class RacClientFactory { + private isInitialized = false; + private logger!: Logger; + private securityPluginSetup?: SecurityPluginSetup; + private securityPluginStart?: SecurityPluginStart; + private getSpaceId!: (request: KibanaRequest) => string | undefined; + private getSpace!: (request: KibanaRequest) => Promise; + private features!: FeaturesPluginStart; + 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; + } + + public create(request: KibanaRequest): 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, + authorization, + auditLogger: securityPluginSetup?.audit.asScoped(request), + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index e6b53a8558964eb..3eecfd6cb3eb0d1 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, @@ -15,6 +15,7 @@ import { } from '../../alerting/common'; import { ActionGroup, AlertExecutorOptions } from '../../alerting/server'; import { RuleRegistry } from './rule_registry'; +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'; @@ -98,3 +99,16 @@ export type RuleType< export type FieldMapOf< TRuleRegistry extends RuleRegistry > = TRuleRegistry extends RuleRegistry ? TFieldMap : never; +/** + * @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 21cf2421ce1b202..9a46fa08208a726 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';