-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[Response Ops][Connectors] Add unsecured actions client to allow system to schedule email action #143282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Response Ops][Connectors] Add unsecured actions client to allow system to schedule email action #143282
Changes from 1 commit
2a93a37
2fe5ce5
dbed0a8
00c611d
b0fba11
baf051a
96b696f
6cf7a3a
fa4a356
8865a80
7c884ae
f4c1851
9a58040
47f1ca9
c87baee
28c6632
db2dc2a
d5bf4ff
026646a
802dfa0
af826cb
557d5f0
943ac49
7bf6884
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,160 @@ | ||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||
| * 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 { compact } from 'lodash'; | ||||||||||||||||||||||||
| import { ISavedObjectsRepository, SavedObjectsBulkResponse } from '@kbn/core/server'; | ||||||||||||||||||||||||
| import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; | ||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
| ActionTypeRegistryContract as ConnectorTypeRegistryContract, | ||||||||||||||||||||||||
| PreConfiguredAction as PreconfiguredConnector, | ||||||||||||||||||||||||
| } from './types'; | ||||||||||||||||||||||||
| import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; | ||||||||||||||||||||||||
| import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; | ||||||||||||||||||||||||
| import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; | ||||||||||||||||||||||||
| import { RelatedSavedObjects } from './lib/related_saved_objects'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface CreateBulkUnsecuredExecuteFunctionOptions { | ||||||||||||||||||||||||
| taskManager: TaskManagerStartContract; | ||||||||||||||||||||||||
| isESOCanEncrypt: boolean; | ||||||||||||||||||||||||
| connectorTypeRegistry: ConnectorTypeRegistryContract; | ||||||||||||||||||||||||
| preconfiguredConnectors: PreconfiguredConnector[]; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export interface ExecuteOptions extends Pick<ActionExecutorOptions, 'params' | 'source'> { | ||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||
| spaceId: string; | ||||||||||||||||||||||||
| apiKey: string | null; | ||||||||||||||||||||||||
| executionId: string; | ||||||||||||||||||||||||
| consumer?: string; | ||||||||||||||||||||||||
| relatedSavedObjects?: RelatedSavedObjects; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export interface ActionTaskParams extends Pick<ActionExecutorOptions, 'params'> { | ||||||||||||||||||||||||
| actionId: string; | ||||||||||||||||||||||||
| apiKey: string | null; | ||||||||||||||||||||||||
| executionId: string; | ||||||||||||||||||||||||
| consumer?: string; | ||||||||||||||||||||||||
| relatedSavedObjects?: RelatedSavedObjects; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export type BulkUnsecuredExecutionEnqueuer<T> = ( | ||||||||||||||||||||||||
| internalSavedObjectsRepository: ISavedObjectsRepository, | ||||||||||||||||||||||||
| actionsToExectute: ExecuteOptions[] | ||||||||||||||||||||||||
| ) => Promise<T>; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export function createBulkUnsecuredExecutionEnqueuerFunction({ | ||||||||||||||||||||||||
| taskManager, | ||||||||||||||||||||||||
| connectorTypeRegistry, | ||||||||||||||||||||||||
| isESOCanEncrypt, | ||||||||||||||||||||||||
| preconfiguredConnectors, | ||||||||||||||||||||||||
| }: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer<void> { | ||||||||||||||||||||||||
| return async function execute( | ||||||||||||||||||||||||
| internalSavedObjectsRepository: ISavedObjectsRepository, | ||||||||||||||||||||||||
| actionsToExecute: ExecuteOptions[] | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| if (!isESOCanEncrypt) { | ||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||
| `Unable to execute actions 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.` | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
ymao1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const connectorTypeIds: Record<string, string> = {}; | ||||||||||||||||||||||||
| const spaceIds: Record<string, string> = {}; | ||||||||||||||||||||||||
| const connectorIds = [...new Set(actionsToExecute.map((action) => action.id))]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const notPreconfiguredConnectors = connectorIds.filter( | ||||||||||||||||||||||||
| (connectorId) => | ||||||||||||||||||||||||
| preconfiguredConnectors.find((connector) => connector.id === connectorId) == null | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (notPreconfiguredConnectors.length > 0) { | ||||||||||||||||||||||||
| // log warning or throw error? | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const connectors: PreconfiguredConnector[] = compact( | ||||||||||||||||||||||||
| connectorIds.map((connectorId) => | ||||||||||||||||||||||||
| preconfiguredConnectors.find((pConnector) => pConnector.id === connectorId) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| connectors.forEach((connector) => { | ||||||||||||||||||||||||
| const { id, actionTypeId } = connector; | ||||||||||||||||||||||||
| if (!connectorTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { | ||||||||||||||||||||||||
| connectorTypeRegistry.ensureActionTypeEnabled(actionTypeId); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| connectorTypeIds[id] = actionTypeId; | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const actions = await Promise.all( | ||||||||||||||||||||||||
| actionsToExecute.map(async (actionToExecute) => { | ||||||||||||||||||||||||
| // Get saved object references from action ID and relatedSavedObjects | ||||||||||||||||||||||||
| const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( | ||||||||||||||||||||||||
| actionToExecute.id, | ||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||
| actionToExecute.relatedSavedObjects | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| const executionSourceReference = executionSourceAsSavedObjectReferences( | ||||||||||||||||||||||||
| actionToExecute.source | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const taskReferences = []; | ||||||||||||||||||||||||
| if (executionSourceReference.references) { | ||||||||||||||||||||||||
| taskReferences.push(...executionSourceReference.references); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (references) { | ||||||||||||||||||||||||
| taskReferences.push(...references); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| spaceIds[actionToExecute.id] = actionToExecute.spaceId; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| /** | |
| * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in | |
| * {@link SavedObjectsCreateOptions}. | |
| * | |
| * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, | |
| * including the "All spaces" identifier (`'*'`). | |
| * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only | |
| * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. | |
| * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. | |
| */ | |
| initialNamespaces?: string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the action_task_param SO that's created by this, the namespaces field is set to ['default'] which seems ok? I can pass it in explicitly if it's necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know enough about the implications of these params existing in one space or another. By default, everything will live in the default space unless we explicitly say otherwise. For our immediate needs, this is might be ok? If a notification is triggered from a Case Assignment within the marketing space, would it be confusing that the corresponding notification was "sent" from the default space?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gsoldevila WDYT of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@legrego it sounds like notifications are fairly space agnostic so there will not be an indication that the notification came from any particular space, even if the case assignment occurred in a specific space @gsoldevila is that a fair statement? Given that, I think we can default to the default space.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is fine to be on the default space as it is not accessible by the user (is this assumption correct?) and it is controlled by the system.
++ I agree. I wasn't sure if this would be surfaced in the Event Log or not, and whether or not we cared about that discrepancy within the Event Log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I am not sure either 🙂. Maybe @ymao1 can help with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The event log does not make any references to the action_task_param SO. It does contain information about the preconfigured connector but since the preconfigured connectors have no namespace, it does not write out a namespace for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The action executor will be writing event log entries for these executions. Do we need a way to differentiate between these actions and actions initiated by alerting?
For the event log, we already have a different event.action for when connectors are executed via http - execute-via-http, so I think having a separate one for this scenario would be good. execute-via-notification?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect at some point the notification system will care about spaces, and we'll support non-pre-configured connectors that live across spaces, and we can defer figuring this out till then.
A non-pre-configured connector's space will show up in the event log, and presumably always will. Pre-configured connectors likely appear to be in the "default" space today. What happens when we have connectors in multiple spaces, not sure. But that's all about the connector, and it sort of feels like the "space of the notification" is going to be a different thing. If everything was supported I could use connector X that's defined in spaces A and B to perform a notification in space A, but the event log doesn't have a notion of "what space something is executed in". It would probably need to be some new field, if we want to capture it.
ymao1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /* | ||
| * 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 { ISavedObjectsRepository } from '@kbn/core/server'; | ||
| import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client_access_registry'; | ||
| import { | ||
| BulkUnsecuredExecutionEnqueuer, | ||
| ExecuteOptions, | ||
| } from '../create_unsecured_execute_function'; | ||
|
|
||
| export interface UnsecuredActionsClientOpts { | ||
| unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; | ||
| internalSavedObjectsRepository: ISavedObjectsRepository; | ||
| executionEnqueuer: BulkUnsecuredExecutionEnqueuer<void>; | ||
| } | ||
|
|
||
| export class UnsecuredActionsClient { | ||
| private readonly unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; | ||
| private readonly internalSavedObjectsRepository: ISavedObjectsRepository; | ||
| private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer<void>; | ||
|
|
||
| constructor(params: UnsecuredActionsClientOpts) { | ||
| this.unsecuredActionsClientAccessRegistry = params.unsecuredActionsClientAccessRegistry; | ||
| this.executionEnqueuer = params.executionEnqueuer; | ||
| this.internalSavedObjectsRepository = params.internalSavedObjectsRepository; | ||
| } | ||
|
|
||
| public async bulkEnqueueExecution( | ||
| requesterId: string, | ||
| actionsToExecute: ExecuteOptions[] | ||
| ): Promise<void> { | ||
| // Check that requesterId is allowed | ||
| if (!this.unsecuredActionsClientAccessRegistry.has(requesterId)) { | ||
| throw new Error( | ||
| `${requesterId} feature is not registered for UnsecuredActionsClient access.` | ||
| ); | ||
| } | ||
| return this.executionEnqueuer(this.internalSavedObjectsRepository, actionsToExecute); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| export class UnsecuredActionsClientAccessRegistry { | ||
| private readonly allowedFeatureIds: Map<string, boolean> = new Map(); | ||
|
|
||
| /** | ||
| * Returns if the access registry has the given feature id registered | ||
| */ | ||
| public has(id: string) { | ||
| return this.allowedFeatureIds.has(id); | ||
| } | ||
|
|
||
| /** | ||
| * Registers feature id to the access registry | ||
| */ | ||
| public register(id: string) { | ||
| this.allowedFeatureIds.set(id, true); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.