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

Add notifications plugin, offering basic email service #143303

Merged
merged 61 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
2a93a37
Adding unsecured actions client
ymao1 Oct 13, 2022
dc9829e
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 13, 2022
2fe5ce5
Removing isESOCanEncrypt check
ymao1 Oct 13, 2022
dbed0a8
Only getting actions client when needed in executor
ymao1 Oct 13, 2022
4af9bc6
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 13, 2022
c3ce0b1
First draft
gsoldevila 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
74aa62d
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 17, 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
3b7a388
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 17, 2022
4b7f482
Misc enhancements following PR comments
gsoldevila 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
ea31342
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 19, 2022
802dfa0
Cleanup
ymao1 Oct 19, 2022
c048225
Misc fixes and simplifications
gsoldevila Oct 19, 2022
f149cac
Merge remote-tracking branch 'remotes/ymao/actions/unsecured-client' …
gsoldevila Oct 20, 2022
cd500db
Add missing tsconfig.json
gsoldevila Oct 20, 2022
a293085
[CI] Auto-commit changed files from 'node scripts/build_plugin_list_d…
kibanamachine Oct 20, 2022
ff65387
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 20, 2022
e241d0d
Add dependency to Actions plugin in tsconfig.json
gsoldevila Oct 20, 2022
0079193
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 20, 2022
7f358bf
Separate setup logic from start logic
gsoldevila Oct 20, 2022
8929996
Fix bulkEnqueueExecution params structure
gsoldevila Oct 24, 2022
fd4b2ef
Update README
gsoldevila Oct 24, 2022
c344a61
Add UTs
gsoldevila Oct 24, 2022
b02bd81
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 24, 2022
53575d9
Check license type >platinum for email notifications
gsoldevila Oct 25, 2022
52e7c8c
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 25, 2022
f214324
Fix incorrect UTs
gsoldevila Oct 26, 2022
e0dd0f7
Import types when possible
gsoldevila Oct 26, 2022
8218cc4
Misc enhancements and code cleanup
gsoldevila Oct 26, 2022
0ccf326
Transform factory => provider, update start contract
gsoldevila Oct 27, 2022
a561085
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 27, 2022
2d26740
Fix merge-related issues
gsoldevila Oct 27, 2022
3ed6752
Code cleanup, update README
gsoldevila Oct 27, 2022
9dbe577
Fix TS error
gsoldevila Oct 27, 2022
0238877
Support list of spaces for each related SO
gsoldevila Oct 28, 2022
06ff938
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Oct 28, 2022
dc4c9a6
Accept single spaceId for related SOs. Add convenience mocks
gsoldevila Nov 2, 2022
2654fee
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 2, 2022
1467966
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 2, 2022
091eb74
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 2, 2022
d6e46f5
Fix CI types error
gsoldevila Nov 2, 2022
8d026c5
Address PR remarks
gsoldevila Nov 3, 2022
f275913
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 3, 2022
56f99c9
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 3, 2022
3aef92a
Address PR remarks #2
gsoldevila Nov 3, 2022
439b3e7
Merge branch 'main' into kbn-140743-notifications-api-mvp
gsoldevila Nov 3, 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
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@
"@kbn/monitoring-collection-plugin/*": ["x-pack/plugins/monitoring_collection/*"],
"@kbn/monitoring-plugin": ["x-pack/plugins/monitoring"],
"@kbn/monitoring-plugin/*": ["x-pack/plugins/monitoring/*"],
"@kbn/notifications-plugin": ["x-pack/plugins/notifications"],
"@kbn/notifications-plugin/*": ["x-pack/plugins/notifications/*"],
"@kbn/observability-plugin": ["x-pack/plugins/observability"],
"@kbn/observability-plugin/*": ["x-pack/plugins/observability/*"],
"@kbn/osquery-plugin": ["x-pack/plugins/osquery"],
Expand Down
158 changes: 158 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,158 @@
/*
* 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';

const ALLOWED_CONNECTOR_TYPE_IDS = ['.email', '.slack'];
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
interface CreateBulkUnsecuredExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
connectorTypeRegistry: ConnectorTypeRegistryContract;
preconfiguredConnectors: PreconfiguredConnector[];
}

export interface ExecuteOptions extends Pick<ActionExecutorOptions, 'params' | 'source'> {
id: string;
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({
Copy link
Contributor

@Dosant Dosant Oct 17, 2022

Choose a reason for hiding this comment

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

I am curious what does unsecured vs secured mean? That apiKey is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the idea is to highlight the fact that this API does not require any form of authentication whatsoever (apiKey or request object), so it should be used cautiously.

This is part of the changes that response-ops are performing to support the case assignment email notifications.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Dosant Normally action execution enqueuing is secured via Kibana RBAC where the actions client checks that the user has access to the Actions and Connectors Kibana feature using the request object before allowing execution enqueuing. For this use case, the system will be scheduling the action so there is no user involved in the request.

taskManager,
connectorTypeRegistry,
preconfiguredConnectors,
}: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer<void> {
return async function execute(
internalSavedObjectsRepository: ISavedObjectsRepository,
actionsToExecute: ExecuteOptions[]
) {
const connectorTypeIds: 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) {
throw new Error(
`${notPreconfiguredConnectors.join(
','
)} are not preconfigured connectors and can't be scheduled for unsecured actions execution`
);
}

const connectors: PreconfiguredConnector[] = compact(
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
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);
}

if (!ALLOWED_CONNECTOR_TYPE_IDS.includes(actionTypeId)) {
throw new Error(
`${actionTypeId} actions cannot be scheduled for unsecured actions execution`
);
}

connectorTypeIds[id] = actionTypeId;
});

const actions = await Promise.all(
Copy link
Member

Choose a reason for hiding this comment

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

Probably I miss something but why do we need to Promise.all if we do not await inside the async function?

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right! I copied it from the create_execute_function but it looks like it's not needed there either 🙈 . I will update it in my PR: #143282

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated in this commit: 026646a

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

return {
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
attributes: {
actionId: actionToExecute.id,
params: actionToExecute.params,
apiKey: null,
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: 'default',
actionTaskParamsId: so.id,
},
state: {},
scope: ['actions'],
};
});
await taskManager.bulkSchedule(taskInstances);
};
}

function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorOptions['source']) {
return isSavedObjectExecutionSource(executionSource)
? {
references: [
{
name: 'source',
...executionSource.source,
},
],
}
: {};
}
40 changes: 27 additions & 13 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ export class ActionExecutor {
throw new Error('ActionExecutor not initialized');
}

if (!this.isESOCanEncrypt) {
throw new Error(
`Unable to execute action 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 withSpan(
{
name: `execute_action`,
Expand All @@ -136,11 +130,14 @@ export class ActionExecutor {
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};

const actionInfo = await getActionInfoInternal(
await getActionsClientWithRequest(request, source),
getActionsClientWithRequest,
request,
this.isESOCanEncrypt,
encryptedSavedObjectsClient,
preconfiguredActions,
actionId,
namespace.namespace
namespace.namespace,
source
);

const { actionTypeId, name, config, secrets } = actionInfo;
Expand Down Expand Up @@ -318,11 +315,14 @@ export class ActionExecutor {
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};
if (!this.actionInfo || this.actionInfo.actionId !== actionId) {
this.actionInfo = await getActionInfoInternal(
await getActionsClientWithRequest(request, source),
getActionsClientWithRequest,
request,
this.isESOCanEncrypt,
encryptedSavedObjectsClient,
preconfiguredActions,
actionId,
namespace.namespace
namespace.namespace,
source
);
}
const task = taskInfo
Expand Down Expand Up @@ -368,12 +368,18 @@ interface ActionInfo {
actionId: string;
}

async function getActionInfoInternal(
actionsClient: PublicMethodsOf<ActionsClient>,
async function getActionInfoInternal<Source = unknown>(
getActionsClientWithRequest: (
request: KibanaRequest,
authorizationContext?: ActionExecutionSource<unknown>
) => Promise<PublicMethodsOf<ActionsClient>>,
request: KibanaRequest,
isESOCanEncrypt: boolean,
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
preconfiguredActions: PreConfiguredAction[],
actionId: string,
namespace: string | undefined
namespace: string | undefined,
source?: ActionExecutionSource<Source>
): Promise<ActionInfo> {
// check to see if it's a pre-configured action first
const pcAction = preconfiguredActions.find(
Expand All @@ -389,6 +395,14 @@ async function getActionInfoInternal(
};
}

if (!isESOCanEncrypt) {
throw new Error(
`Unable to execute action 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 actionsClient = await getActionsClientWithRequest(request, source);

// if not pre-configured action, should be a saved object
// ensure user can read the action before processing
const { actionTypeId, config, name } = await actionsClient.get({ id: actionId });
Expand Down
29 changes: 29 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,22 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
});
};

const getUnsecuredActionsClient = async () => {
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!,
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 +560,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);
}
}
Loading