Skip to content

Commit

Permalink
Adding unsecured actions client
Browse files Browse the repository at this point in the history
  • Loading branch information
ymao1 committed Oct 13, 2022
1 parent bc3eadd commit 44f5ef0
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 0 deletions.
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;
}

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.`
);
}

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,
attributes: {
actionId: actionToExecute.id,
params: actionToExecute.params,
apiKey: actionToExecute.apiKey,
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) => {
unsecuredActionsClient.bulkEnqueueExecution('alerting', [
{
id: 'gmail',
params: {
to: ['ying.gu@gmail.com'],
subject: 'hello from Kibana!',
message: 'does this work??',
},
spaceId: 'default',
apiKey: null,
executionId: 'abc',
},
]);
});
return {
listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!),
getAlertingAuthorizationWithRequest,
Expand Down

0 comments on commit 44f5ef0

Please sign in to comment.