Skip to content
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

Merged
merged 24 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2a93a37
Adding unsecured actions client
ymao1 Oct 13, 2022
2fe5ce5
Removing isESOCanEncrypt check
ymao1 Oct 13, 2022
dbed0a8
Only getting actions client when needed in executor
ymao1 Oct 13, 2022
00c611d
Changing to feature id allowlist. Adding unit tests
ymao1 Oct 14, 2022
b0fba11
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 14, 2022
baf051a
Removing execution id
ymao1 Oct 14, 2022
96b696f
Cleanup
ymao1 Oct 14, 2022
6cf7a3a
Fixing unit tests
ymao1 Oct 14, 2022
fa4a356
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 17, 2022
8865a80
Removing slack from allowlist
ymao1 Oct 17, 2022
7c884ae
Make getUnsecuredActionsClient synchronous
ymao1 Oct 17, 2022
f4c1851
Add comment
ymao1 Oct 17, 2022
9a58040
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 18, 2022
47f1ca9
Adding functional tests
ymao1 Oct 18, 2022
c87baee
Fixing types
ymao1 Oct 18, 2022
28c6632
Fixing tests
ymao1 Oct 18, 2022
db2dc2a
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 18, 2022
d5bf4ff
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 19, 2022
026646a
Removing unnecessary Promise.all
ymao1 Oct 19, 2022
802dfa0
Cleanup
ymao1 Oct 19, 2022
af826cb
Merge branch 'main' into actions/unsecured-client
kibanamachine Oct 24, 2022
557d5f0
Merge branch 'main' of https://github.com/elastic/kibana into actions…
ymao1 Oct 26, 2022
943ac49
PR feedback
ymao1 Oct 26, 2022
7bf6884
Merge branch 'actions/unsecured-client' of https://github.com/ymao1/k…
ymao1 Oct 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions x-pack/plugins/actions/server/create_unsecured_execute_function.ts
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;
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
}

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.
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this SO type is namespaceType: 'multiple-isolated', do we need to also be specifying the initialNamespaces property when we create the saved object? Since we're using the bare repository, this won't be populated for you automatically.

/**
* 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[];

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gsoldevila WDYT of this?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Member

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.

attributes: {
actionId: actionToExecute.id,
params: actionToExecute.params,
apiKey: actionToExecute.apiKey,
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
executionId: actionToExecute.executionId,
consumer: actionToExecute.consumer,
relatedSavedObjects: relatedSavedObjectWithRefs,
},
references: taskReferences,
};
})
);

const actionTaskParamsRecords: SavedObjectsBulkResponse<ActionTaskParams> =
await internalSavedObjectsRepository.bulkCreate(actions);

const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => {
const actionId = so.attributes.actionId;
return {
taskType: `actions:${connectorTypeIds[actionId]}`,
params: {
spaceId: spaceIds[actionId],
actionTaskParamsId: so.id,
},
state: {},
scope: ['actions'],
};
});
await taskManager.bulkSchedule(taskInstances);
};
}

function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorOptions['source']) {
return isSavedObjectExecutionSource(executionSource)
? {
references: [
{
name: 'source',
...executionSource.source,
},
],
}
: {};
}
36 changes: 36 additions & 0 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ import { createSubActionConnectorFramework } from './sub_action_framework';
import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types';
import { SubActionConnector } from './sub_action_framework/sub_action_connector';
import { CaseConnector } from './sub_action_framework/case';
import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client/unsecured_actions_client_access_registry';
import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client';
import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function';

export interface PluginSetupContract {
registerType<
Expand All @@ -117,6 +120,7 @@ export interface PluginSetupContract {
>(
connector: SubActionConnectorType<Config, Secrets>
): void;
registerUnsecuredActionsClientAccess(featureId: string): void;
isPreconfiguredConnector(connectorId: string): boolean;
getSubActionConnectorClass: <Config, Secrets>() => IServiceAbstract<Config, Secrets>;
getCaseConnectorClass: <Config, Secrets>() => IServiceAbstract<Config, Secrets>;
Expand All @@ -138,6 +142,8 @@ export interface PluginStartContract {

preconfiguredActions: PreConfiguredAction[];

getUnsecuredActionsClient(): Promise<PublicMethodsOf<UnsecuredActionsClient>>;

renderActionParameterTemplates<Params extends ActionTypeParams = ActionTypeParams>(
actionTypeId: string,
actionId: string,
Expand Down Expand Up @@ -188,6 +194,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
private readonly preconfiguredActions: PreConfiguredAction[];
private inMemoryMetrics: InMemoryMetrics;
private kibanaIndex?: string;
private unsecuredActionsClientAccessRegistry?: UnsecuredActionsClientAccessRegistry;

constructor(initContext: PluginInitializerContext) {
this.logger = initContext.logger.get();
Expand Down Expand Up @@ -266,6 +273,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
this.actionExecutor = actionExecutor;
this.security = plugins.security;

this.unsecuredActionsClientAccessRegistry = new UnsecuredActionsClientAccessRegistry();

setupSavedObjects(
core.savedObjects,
plugins.encryptedSavedObjects,
Expand Down Expand Up @@ -361,6 +370,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
) => {
subActionFramework.registerConnector(connector);
},
registerUnsecuredActionsClientAccess: (featureId: string) => {
this.unsecuredActionsClientAccessRegistry?.register(featureId);
},
isPreconfiguredConnector: (connectorId: string): boolean => {
return !!this.preconfiguredActions.find(
(preconfigured) => preconfigured.id === connectorId
Expand Down Expand Up @@ -452,6 +464,29 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
});
};

const getUnsecuredActionsClient = async () => {
if (isESOCanEncrypt !== true) {
throw new Error(
`Unable to create unsecured actions 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.`
);
}

const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
]);

return new UnsecuredActionsClient({
internalSavedObjectsRepository,
unsecuredActionsClientAccessRegistry: this.unsecuredActionsClientAccessRegistry!,
executionEnqueuer: createBulkUnsecuredExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
connectorTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredConnectors: preconfiguredActions,
}),
});
};

// Ensure the public API cannot be used to circumvent authorization
// using our legacy exemption mechanism by passing in a legacy SO
// as authorizationContext which would then set a Legacy AuthorizationMode
Expand Down Expand Up @@ -532,6 +567,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
return instantiateAuthorization(request);
},
getActionsClientWithRequest: secureGetActionsClientWithRequest,
getUnsecuredActionsClient,
preconfiguredActions,
renderActionParameterTemplates: (...args) =>
renderActionParameterTemplates(actionTypeRegistry, ...args),
Expand Down
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);
}
}
17 changes: 17 additions & 0 deletions x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ export class AlertingPlugin {
this.eventLogService = plugins.eventLog;
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));

plugins.actions.registerUnsecuredActionsClientAccess('alerting');

const ruleTypeRegistry = new RuleTypeRegistry({
logger: this.logger,
taskManager: plugins.taskManager,
Expand Down Expand Up @@ -462,6 +464,21 @@ export class AlertingPlugin {
scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager);
scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager);

plugins.actions.getUnsecuredActionsClient().then((unsecuredActionsClient) => {
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
unsecuredActionsClient.bulkEnqueueExecution('alerting', [
{
id: 'gmail',
params: {
to: ['xxxxxx'],
subject: 'hello from Kibana!',
message: 'does this work??',
},
spaceId: 'default',
apiKey: null,
executionId: 'abc',
},
]);
});
return {
listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!),
getAlertingAuthorizationWithRequest,
Expand Down