Skip to content

Commit

Permalink
[ResponseOps] Prepare the connector execute HTTP API for versioning (
Browse files Browse the repository at this point in the history
…elastic#194481)

Towards elastic/response-ops-team#125

## Summary

Preparing the `POST ${BASE_ACTION_API_PATH}/connector/{id}/_execute`
HTTP API for versioning

---------

Co-authored-by: Ying Mao <ying.mao@elastic.co>
  • Loading branch information
doakalexi and ymao1 authored Oct 16, 2024
1 parent 50cf1b3 commit 241a05a
Show file tree
Hide file tree
Showing 33 changed files with 426 additions and 164 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 {
executeConnectorRequestParamsSchema,
executeConnectorRequestBodySchema,
} from './schemas/latest';
export type { ExecuteConnectorRequestParams, ExecuteConnectorRequestBody } from './types/latest';

export {
executeConnectorRequestParamsSchema as executeConnectorRequestParamsSchemaV1,
executeConnectorRequestBodySchema as executeConnectorRequestBodySchemaV1,
} from './schemas/v1';
export type {
ExecuteConnectorRequestParams as ExecuteConnectorRequestParamsV1,
ExecuteConnectorRequestBody as ExecuteConnectorRequestBodyV1,
} from './types/v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { schema } from '@kbn/config-schema';

export const executeConnectorRequestParamsSchema = schema.object({
id: schema.string({
meta: {
description: 'An identifier for the connector.',
},
}),
});

export const executeConnectorRequestBodySchema = schema.object({
params: schema.recordOf(schema.string(), schema.any()),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { executeConnectorRequestParamsSchemaV1, executeConnectorRequestBodySchemaV1 } from '..';

export type ExecuteConnectorRequestParams = TypeOf<typeof executeConnectorRequestParamsSchemaV1>;
export type ExecuteConnectorRequestBody = TypeOf<typeof executeConnectorRequestBodySchemaV1>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,28 @@
*/

// Latest
export type { ConnectorResponse, AllConnectorsResponse } from './types/latest';
export type {
ConnectorResponse,
AllConnectorsResponse,
ConnectorExecuteResponse,
} from './types/latest';
export {
connectorResponseSchema,
allConnectorsResponseSchema,
connectorTypesResponseSchema,
connectorExecuteResponseSchema,
} from './schemas/latest';

// v1
export type {
ConnectorResponse as ConnectorResponseV1,
AllConnectorsResponse as AllConnectorsResponseV1,
ConnectorTypesResponse as ConnectorTypesResponseV1,
ConnectorExecuteResponse as ConnectorExecuteResponseV1,
} from './types/v1';
export {
connectorResponseSchema as connectorResponseSchemaV1,
allConnectorsResponseSchema as connectorWithExtraFindDataSchemaV1,
connectorTypesResponseSchema as connectorTypesResponseSchemaV1,
connectorExecuteResponseSchema as connectorExecuteResponseSchemaV1,
} from './schemas/v1';
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export { connectorResponseSchema } from './v1';
export { allConnectorsResponseSchema } from './v1';
export { connectorTypesResponseSchema } from './v1';
export { connectorExecuteResponseSchema } from './v1';
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,55 @@ export const connectorTypesResponseSchema = schema.object({
meta: { description: 'Indicates whether the action is a system action.' },
}),
});

export const connectorExecuteResponseSchema = schema.object({
connector_id: schema.string({
meta: {
description: 'The identifier for the connector.',
},
}),
status: schema.oneOf([schema.literal('ok'), schema.literal('error')], {
meta: {
description: 'The outcome of the connector execution.',
},
}),
message: schema.maybe(
schema.string({
meta: {
description: 'The connector execution error message.',
},
})
),
service_message: schema.maybe(
schema.string({
meta: {
description: 'An error message that contains additional details.',
},
})
),
data: schema.maybe(
schema.any({
meta: {
description: 'The connector execution data.',
},
})
),
retry: schema.maybe(
schema.nullable(
schema.oneOf([schema.boolean(), schema.string()], {
meta: {
description:
'When the status is error, identifies whether the connector execution will retry .',
},
})
)
),
errorSource: schema.maybe(
schema.oneOf([schema.literal('user'), schema.literal('framework')], {
meta: {
description:
'When the status is error, identifies whether the error is a framework error or a user error.',
},
})
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
connectorResponseSchemaV1,
connectorTypesResponseSchemaV1,
allConnectorsResponseSchema,
connectorExecuteResponseSchema,
} from '..';

type ConnectorResponseSchemaType = TypeOf<typeof connectorResponseSchemaV1>;
Expand Down Expand Up @@ -41,3 +42,14 @@ export interface ConnectorTypesResponse {
supported_feature_ids: ConnectorTypesResponseSchemaType['supported_feature_ids'];
is_system_action_type: ConnectorTypesResponseSchemaType['is_system_action_type'];
}

type ConnectorExecuteResponseSchemaType = TypeOf<typeof connectorExecuteResponseSchema>;
export interface ConnectorExecuteResponse {
connector_id: ConnectorExecuteResponseSchemaType['connector_id'];
status: ConnectorExecuteResponseSchemaType['status'];
message?: ConnectorExecuteResponseSchemaType['message'];
service_message?: ConnectorExecuteResponseSchemaType['service_message'];
data?: ConnectorExecuteResponseSchemaType['data'];
retry?: ConnectorExecuteResponseSchemaType['retry'];
errorSource?: ConnectorExecuteResponseSchemaType['errorSource'];
}
87 changes: 10 additions & 77 deletions x-pack/plugins/actions/server/actions_client/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import { v4 as uuidv4 } from 'uuid';
import Boom from '@hapi/boom';
import url from 'url';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
Expand All @@ -30,6 +29,7 @@ import { get } from '../application/connector/methods/get';
import { getAll, getAllSystemConnectors } from '../application/connector/methods/get_all';
import { update } from '../application/connector/methods/update';
import { listTypes } from '../application/connector/methods/list_types';
import { execute } from '../application/connector/methods/execute';
import {
GetGlobalExecutionKPIParams,
GetGlobalExecutionLogParams,
Expand All @@ -54,7 +54,6 @@ import {
HookServices,
} from '../types';
import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification';
import { ExecuteOptions } from '../lib/action_executor';
import {
ExecutionEnqueuer,
ExecuteOptions as EnqueueExecutionOptions,
Expand Down Expand Up @@ -96,6 +95,9 @@ import { connectorFromSavedObject, isConnectorDeprecated } from '../application/
import { ListTypesParams } from '../application/connector/methods/list_types/types';
import { ConnectorUpdateParams } from '../application/connector/methods/update/types';
import { ConnectorUpdate } from '../application/connector/methods/update/types/types';
import { isPreconfigured } from '../lib/is_preconfigured';
import { isSystemAction } from '../lib/is_system_action';
import { ConnectorExecuteParams } from '../application/connector/methods/execute/types';

interface Action extends ConnectorUpdate {
actionTypeId: string;
Expand Down Expand Up @@ -649,75 +651,10 @@ export class ActionsClient {
return result;
}

private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) {
const inMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === connectorId
);

const additionalPrivileges = inMemoryConnector?.isSystemAction
? this.context.actionTypeRegistry.getSystemActionKibanaPrivileges(
inMemoryConnector.actionTypeId,
params
)
: [];

return additionalPrivileges;
}

public async execute({
actionId,
params,
source,
relatedSavedObjects,
}: Omit<ExecuteOptions, 'request' | 'actionExecutionId'>): Promise<
ActionTypeExecutorResult<unknown>
> {
const log = this.context.logger;

if (
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params);
let actionTypeId: string | undefined;

try {
if (this.isPreconfigured(actionId) || this.isSystemAction(actionId)) {
const connector = this.context.inMemoryConnectors.find(
(inMemoryConnector) => inMemoryConnector.id === actionId
);

actionTypeId = connector?.actionTypeId;
} else {
// TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute
const { attributes } = await this.context.unsecuredSavedObjectsClient.get<RawAction>(
'action',
actionId
);

actionTypeId = attributes.actionTypeId;
}
} catch (err) {
log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err);
}

await this.context.authorization.ensureAuthorized({
operation: 'execute',
additionalPrivileges,
actionTypeId,
});
} else {
trackLegacyRBACExemption('execute', this.context.usageCounter);
}

return this.context.actionExecutor.execute({
actionId,
params,
source,
request: this.context.request,
relatedSavedObjects,
actionExecutionId: uuidv4(),
});
public async execute(
connectorExecuteParams: ConnectorExecuteParams
): Promise<ActionTypeExecutorResult<unknown>> {
return execute(this.context, connectorExecuteParams);
}

public async bulkEnqueueExecution(
Expand Down Expand Up @@ -789,15 +726,11 @@ export class ActionsClient {
}

public isPreconfigured(connectorId: string): boolean {
return !!this.context.inMemoryConnectors.find(
(connector) => connector.isPreconfigured && connector.id === connectorId
);
return isPreconfigured(this.context, connectorId);
}

public isSystemAction(connectorId: string): boolean {
return !!this.context.inMemoryConnectors.find(
(connector) => connector.isSystemAction && connector.id === connectorId
);
return isSystemAction(this.context, connectorId);
}

public async getGlobalExecutionLogWithAuth({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import { RawAction, ActionTypeExecutorResult } from '../../../../types';
import { getSystemActionKibanaPrivileges } from '../../../../lib/get_system_action_kibana_privileges';
import { isPreconfigured } from '../../../../lib/is_preconfigured';
import { isSystemAction } from '../../../../lib/is_system_action';
import {
getAuthorizationModeBySource,
AuthorizationMode,
} from '../../../../authorization/get_authorization_mode_by_source';
import { trackLegacyRBACExemption } from '../../../../lib/track_legacy_rbac_exemption';
import { ConnectorExecuteParams } from './types';
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../constants/saved_objects';
import { ActionsClientContext } from '../../../../actions_client';

export async function execute(
context: ActionsClientContext,
connectorExecuteParams: ConnectorExecuteParams
): Promise<ActionTypeExecutorResult<unknown>> {
const log = context.logger;
const { actionId, params, source, relatedSavedObjects } = connectorExecuteParams;

if (
(await getAuthorizationModeBySource(context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params);
let actionTypeId: string | undefined;

try {
if (isPreconfigured(context, actionId) || isSystemAction(context, actionId)) {
const connector = context.inMemoryConnectors.find(
(inMemoryConnector) => inMemoryConnector.id === actionId
);

actionTypeId = connector?.actionTypeId;
} else {
// TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute
const { attributes } = await context.unsecuredSavedObjectsClient.get<RawAction>(
ACTION_SAVED_OBJECT_TYPE,
actionId
);

actionTypeId = attributes.actionTypeId;
}
} catch (err) {
log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err);
}

await context.authorization.ensureAuthorized({
operation: 'execute',
additionalPrivileges,
actionTypeId,
});
} else {
trackLegacyRBACExemption('execute', context.usageCounter);
}

return context.actionExecutor.execute({
actionId,
params,
source,
request: context.request,
relatedSavedObjects,
actionExecutionId: uuidv4(),
});
}
Loading

0 comments on commit 241a05a

Please sign in to comment.