Prepare the connector GetAll API for versioning (#162799)

Part of: https://github.com/elastic/response-ops-team/issues/125

This PR intends to prepare the `GET ${BASE_ACTION_API_PATH}/connectors`
API for versioning as shown in the above issue.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ersin Erdal 2023-08-23 16:33:07 +03:00 committed by GitHub
parent 0d85af23a4
commit d65b02cf06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1314 additions and 730 deletions

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
// Latest
export type { ConnectorResponse, ActionTypeConfig } from './types/latest';
export { connectorResponseSchema } from './schemas/latest';
// v1
export type {
ConnectorResponse as ConnectorResponseV1,
ActionTypeConfig as ActionTypeConfigV1,
} from './types/v1';
export { connectorResponseSchema as connectorResponseSchemaV1 } from './schemas/v1';

View file

@ -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 { connectorResponseSchema } from './v1';

View file

@ -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 connectorResponseSchema = schema.object({
id: schema.string(),
name: schema.string(),
config: schema.maybe(schema.recordOf(schema.string(), schema.any())),
connector_type_id: schema.string(),
is_missing_secrets: schema.maybe(schema.boolean()),
is_preconfigured: schema.boolean(),
is_deprecated: schema.boolean(),
is_system_action: schema.boolean(),
referenced_by_count: schema.number(),
});

View file

@ -0,0 +1,7 @@
/*
* 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';

View file

@ -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.
*/
import { TypeOf } from '@kbn/config-schema';
import { connectorResponseSchemaV1 } from '..';
export type ActionTypeConfig = Record<string, unknown>;
type ConnectorResponseSchemaType = TypeOf<typeof connectorResponseSchemaV1>;
export interface ConnectorResponse<Config extends ActionTypeConfig = ActionTypeConfig> {
id: ConnectorResponseSchemaType['id'];
name: ConnectorResponseSchemaType['name'];
config?: Config;
connector_type_id: ConnectorResponseSchemaType['connector_type_id'];
is_missing_secrets?: ConnectorResponseSchemaType['is_missing_secrets'];
is_preconfigured: ConnectorResponseSchemaType['is_preconfigured'];
is_deprecated: ConnectorResponseSchemaType['is_deprecated'];
is_system_action: ConnectorResponseSchemaType['is_system_action'];
referenced_by_count: ConnectorResponseSchemaType['referenced_by_count'];
}

View file

@ -9,19 +9,19 @@ import { schema } from '@kbn/config-schema';
import moment from 'moment';
import { ByteSizeValue } from '@kbn/config-schema';
import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry';
import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry';
import { ActionsClient } from './actions_client';
import { ExecutorType, ActionType } from './types';
import { ExecutorType, ActionType } from '../types';
import {
ActionExecutor,
TaskRunnerFactory,
ILicenseState,
asHttpRequestExecutionSource,
} from './lib';
} from '../lib';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { actionsConfigMock } from '../actions_config.mock';
import { getActionsConfigurationUtilities } from '../actions_config';
import { licenseStateMock } from '../lib/license_state.mock';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import {
httpServerMock,
@ -31,26 +31,26 @@ import {
} from '@kbn/core/server/mocks';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
import { actionExecutorMock } from './lib/action_executor.mock';
import { actionExecutorMock } from '../lib/action_executor.mock';
import { v4 as uuidv4 } from 'uuid';
import { ActionsAuthorization } from './authorization/actions_authorization';
import { ActionsAuthorization } from '../authorization/actions_authorization';
import {
getAuthorizationModeBySource,
AuthorizationMode,
getBulkAuthorizationModeBySource,
} from './authorization/get_authorization_mode_by_source';
import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption';
import { ConnectorTokenClient } from './lib/connector_token_client';
} from '../authorization/get_authorization_mode_by_source';
import { actionsAuthorizationMock } from '../authorization/actions_authorization.mock';
import { trackLegacyRBACExemption } from '../lib/track_legacy_rbac_exemption';
import { ConnectorTokenClient } from '../lib/connector_token_client';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { Logger } from '@kbn/core/server';
import { connectorTokenClientMock } from './lib/connector_token_client.mock';
import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
import { getOAuthJwtAccessToken } from './lib/get_oauth_jwt_access_token';
import { getOAuthClientCredentialsAccessToken } from './lib/get_oauth_client_credentials_access_token';
import { OAuthParams } from './routes/get_oauth_access_token';
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token';
import { getOAuthClientCredentialsAccessToken } from '../lib/get_oauth_client_credentials_access_token';
import { OAuthParams } from '../routes/get_oauth_access_token';
import { eventLogClientMock } from '@kbn/event-log-plugin/server/event_log_client.mock';
import { GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams } from '../common';
import { GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams } from '../../common';
jest.mock('@kbn/core-saved-objects-utils-server', () => {
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
@ -62,11 +62,11 @@ jest.mock('@kbn/core-saved-objects-utils-server', () => {
};
});
jest.mock('./lib/track_legacy_rbac_exemption', () => ({
jest.mock('../lib/track_legacy_rbac_exemption', () => ({
trackLegacyRBACExemption: jest.fn(),
}));
jest.mock('./authorization/get_authorization_mode_by_source', () => {
jest.mock('../authorization/get_authorization_mode_by_source', () => {
return {
getAuthorizationModeBySource: jest.fn(() => {
return 1;
@ -81,10 +81,10 @@ jest.mock('./authorization/get_authorization_mode_by_source', () => {
};
});
jest.mock('./lib/get_oauth_jwt_access_token', () => ({
jest.mock('../lib/get_oauth_jwt_access_token', () => ({
getOAuthJwtAccessToken: jest.fn(),
}));
jest.mock('./lib/get_oauth_client_credentials_access_token', () => ({
jest.mock('../lib/get_oauth_client_credentials_access_token', () => ({
getOAuthClientCredentialsAccessToken: jest.fn(),
}));
@ -1242,364 +1242,6 @@ describe('get()', () => {
});
});
describe('getAll()', () => {
describe('authorization', () => {
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
return actionsClient.getAll();
}
test('ensures user is authorised to get the type of action', async () => {
await getAllOperation();
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
});
test('throws when user is not authorised to create the type of action', async () => {
authorization.ensureAuthorized.mockRejectedValue(
new Error(`Unauthorized to get all actions`)
);
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
`[Error: Unauthorized to get all actions]`
);
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
});
});
describe('auditLogger', () => {
test('logs audit event when searching connectors', async () => {
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
});
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
}
);
await actionsClient.getAll();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'connector_find',
outcome: 'success',
}),
kibana: { saved_object: { id: '1', type: 'action' } },
})
);
});
test('logs audit event when not authorised to search connectors', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(actionsClient.getAll()).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'connector_find',
outcome: 'failure',
}),
error: { code: 'Error', message: 'Unauthorized' },
})
);
});
});
test('calls unsecuredSavedObjectsClient with parameters and returns inMemoryConnectors correctly', async () => {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
'system-connector-.cases': { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
/**
* System actions will not
* be returned from getAll
* if no options are provided
*/
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
config: {},
secrets: {},
isDeprecated: false,
isMissingSecrets: false,
isPreconfigured: false,
isSystemAction: true,
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
const result = await actionsClient.getAll();
expect(result).toEqual([
{
id: '1',
name: 'test',
isMissingSecrets: false,
config: { foo: 'bar' },
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
referencedByCount: 6,
},
{
id: 'testPreconfigured',
actionTypeId: '.slack',
name: 'test',
isPreconfigured: true,
isSystemAction: false,
isDeprecated: false,
referencedByCount: 2,
},
]);
});
test('get system actions correctly', async () => {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
'system-connector-.cases': { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
config: {},
secrets: {},
isDeprecated: false,
isMissingSecrets: false,
isPreconfigured: false,
isSystemAction: true,
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
const result = await actionsClient.getAll({ includeSystemActions: true });
expect(result).toEqual([
{
actionTypeId: '.cases',
id: 'system-connector-.cases',
isDeprecated: false,
isPreconfigured: false,
isSystemAction: true,
name: 'System action: .cases',
referencedByCount: 2,
},
{
id: '1',
name: 'test',
isMissingSecrets: false,
config: { foo: 'bar' },
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
referencedByCount: 6,
},
{
id: 'testPreconfigured',
actionTypeId: '.slack',
name: 'test',
isPreconfigured: true,
isSystemAction: false,
isDeprecated: false,
referencedByCount: 2,
},
]);
});
});
describe('getBulk()', () => {
describe('authorization', () => {
function getBulkOperation(): ReturnType<ActionsClient['getBulk']> {

View file

@ -8,7 +8,6 @@
import { v4 as uuidv4 } from 'uuid';
import Boom from '@hapi/boom';
import url from 'url';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { i18n } from '@kbn/i18n';
@ -17,7 +16,6 @@ import {
IScopedClusterClient,
SavedObjectsClientContract,
SavedObjectAttributes,
SavedObject,
KibanaRequest,
SavedObjectsUtils,
Logger,
@ -26,13 +24,15 @@ import { AuditLogger } from '@kbn/security-plugin/server';
import { RunNowResult } from '@kbn/task-manager-plugin/server';
import { IEventLogClient } from '@kbn/event-log-plugin/server';
import { KueryNode } from '@kbn/es-query';
import { FindConnectorResult } from '../application/connector/types';
import { getAll } from '../application/connector/methods/get_all';
import {
ActionType,
GetGlobalExecutionKPIParams,
GetGlobalExecutionLogParams,
IExecutionLogResult,
} from '../common';
import { ActionTypeRegistry } from './action_type_registry';
} from '../../common';
import { ActionTypeRegistry } from '../action_type_registry';
import {
validateConfig,
validateSecrets,
@ -40,59 +40,54 @@ import {
validateConnector,
ActionExecutionSource,
parseDate,
} from './lib';
} from '../lib';
import {
ActionResult,
FindActionResult,
RawAction,
InMemoryConnector,
ActionTypeExecutorResult,
ConnectorTokenClientContract,
} from './types';
import { PreconfiguredActionDisabledModificationError } from './lib/errors/preconfigured_action_disabled_modification';
import { ExecuteOptions } from './lib/action_executor';
} from '../types';
import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification';
import { ExecuteOptions } from '../lib/action_executor';
import {
ExecutionEnqueuer,
ExecuteOptions as EnqueueExecutionOptions,
BulkExecutionEnqueuer,
} from './create_execute_function';
import { ActionsAuthorization } from './authorization/actions_authorization';
} from '../create_execute_function';
import { ActionsAuthorization } from '../authorization/actions_authorization';
import {
getAuthorizationModeBySource,
getBulkAuthorizationModeBySource,
AuthorizationMode,
} from './authorization/get_authorization_mode_by_source';
import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events';
import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption';
import { isConnectorDeprecated } from './lib/is_connector_deprecated';
import { ActionsConfigurationUtilities } from './actions_config';
} from '../authorization/get_authorization_mode_by_source';
import { connectorAuditEvent, ConnectorAuditAction } from '../lib/audit_events';
import { trackLegacyRBACExemption } from '../lib/track_legacy_rbac_exemption';
import { ActionsConfigurationUtilities } from '../actions_config';
import {
OAuthClientCredentialsParams,
OAuthJwtParams,
OAuthParams,
} from './routes/get_oauth_access_token';
} from '../routes/get_oauth_access_token';
import {
getOAuthJwtAccessToken,
GetOAuthJwtConfig,
GetOAuthJwtSecrets,
} from './lib/get_oauth_jwt_access_token';
} from '../lib/get_oauth_jwt_access_token';
import {
getOAuthClientCredentialsAccessToken,
GetOAuthClientCredentialsConfig,
GetOAuthClientCredentialsSecrets,
} from './lib/get_oauth_client_credentials_access_token';
} from '../lib/get_oauth_client_credentials_access_token';
import {
ACTION_FILTER,
formatExecutionKPIResult,
formatExecutionLogResult,
getExecutionKPIAggregation,
getExecutionLogAggregation,
} from './lib/get_execution_log_aggregation';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
// We'll set this max setting assuming it's never reached.
export const MAX_ACTIONS_RETURNED = 10000;
} from '../lib/get_execution_log_aggregation';
import { connectorFromSavedObject, isConnectorDeprecated } from '../application/connector/lib';
interface ActionUpdate {
name: string;
@ -133,32 +128,32 @@ export interface UpdateOptions {
action: ActionUpdate;
}
interface GetAllOptions {
includeSystemActions?: boolean;
}
interface ListTypesOptions {
featureId?: string;
includeSystemActionTypes?: boolean;
}
export interface ActionsClientContext {
logger: Logger;
kibanaIndices: string[];
scopedClusterClient: IScopedClusterClient;
unsecuredSavedObjectsClient: SavedObjectsClientContract;
actionTypeRegistry: ActionTypeRegistry;
inMemoryConnectors: InMemoryConnector[];
actionExecutor: ActionExecutorContract;
request: KibanaRequest;
authorization: ActionsAuthorization;
executionEnqueuer: ExecutionEnqueuer<void>;
ephemeralExecutionEnqueuer: ExecutionEnqueuer<RunNowResult>;
bulkExecutionEnqueuer: BulkExecutionEnqueuer<void>;
auditLogger?: AuditLogger;
usageCounter?: UsageCounter;
connectorTokenClient: ConnectorTokenClientContract;
getEventLogClient: () => Promise<IEventLogClient>;
}
export class ActionsClient {
private readonly logger: Logger;
private readonly kibanaIndices: string[];
private readonly scopedClusterClient: IScopedClusterClient;
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
private readonly actionTypeRegistry: ActionTypeRegistry;
private readonly inMemoryConnectors: InMemoryConnector[];
private readonly actionExecutor: ActionExecutorContract;
private readonly request: KibanaRequest;
private readonly authorization: ActionsAuthorization;
private readonly executionEnqueuer: ExecutionEnqueuer<void>;
private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer<RunNowResult>;
private readonly bulkExecutionEnqueuer: BulkExecutionEnqueuer<void>;
private readonly auditLogger?: AuditLogger;
private readonly usageCounter?: UsageCounter;
private readonly connectorTokenClient: ConnectorTokenClientContract;
private readonly getEventLogClient: () => Promise<IEventLogClient>;
private readonly context: ActionsClientContext;
constructor({
logger,
@ -178,22 +173,24 @@ export class ActionsClient {
connectorTokenClient,
getEventLogClient,
}: ConstructorOptions) {
this.logger = logger;
this.actionTypeRegistry = actionTypeRegistry;
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
this.scopedClusterClient = scopedClusterClient;
this.kibanaIndices = kibanaIndices;
this.inMemoryConnectors = inMemoryConnectors;
this.actionExecutor = actionExecutor;
this.executionEnqueuer = executionEnqueuer;
this.ephemeralExecutionEnqueuer = ephemeralExecutionEnqueuer;
this.bulkExecutionEnqueuer = bulkExecutionEnqueuer;
this.request = request;
this.authorization = authorization;
this.auditLogger = auditLogger;
this.usageCounter = usageCounter;
this.connectorTokenClient = connectorTokenClient;
this.getEventLogClient = getEventLogClient;
this.context = {
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
inMemoryConnectors,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization,
auditLogger,
usageCounter,
connectorTokenClient,
getEventLogClient,
};
}
/**
@ -206,9 +203,12 @@ export class ActionsClient {
const id = options?.id || SavedObjectsUtils.generateId();
try {
await this.authorization.ensureAuthorized({ operation: 'create', actionTypeId });
await this.context.authorization.ensureAuthorized({
operation: 'create',
actionTypeId,
});
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.CREATE,
savedObject: { type: 'action', id },
@ -218,10 +218,12 @@ export class ActionsClient {
throw error;
}
const foundInMemoryConnector = this.inMemoryConnectors.find((connector) => connector.id === id);
const foundInMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === id
);
if (
this.actionTypeRegistry.isSystemActionType(actionTypeId) ||
this.context.actionTypeRegistry.isSystemActionType(actionTypeId) ||
foundInMemoryConnector?.isSystemAction
) {
throw Boom.badRequest(
@ -245,9 +247,8 @@ export class ActionsClient {
);
}
const actionType = this.actionTypeRegistry.get(actionTypeId);
const configurationUtilities = this.actionTypeRegistry.getUtils();
const actionType = this.context.actionTypeRegistry.get(actionTypeId);
const configurationUtilities = this.context.actionTypeRegistry.getUtils();
const validatedActionTypeConfig = validateConfig(actionType, config, {
configurationUtilities,
});
@ -257,9 +258,9 @@ export class ActionsClient {
if (actionType.validate?.connector) {
validateConnector(actionType, { config, secrets });
}
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
this.context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.CREATE,
savedObject: { type: 'action', id },
@ -267,7 +268,7 @@ export class ActionsClient {
})
);
const result = await this.unsecuredSavedObjectsClient.create(
const result = await this.context.unsecuredSavedObjectsClient.create(
'action',
{
actionTypeId,
@ -296,9 +297,9 @@ export class ActionsClient {
*/
public async update({ id, action }: UpdateOptions): Promise<ActionResult> {
try {
await this.authorization.ensureAuthorized({ operation: 'update' });
await this.context.authorization.ensureAuthorized({ operation: 'update' });
const foundInMemoryConnector = this.inMemoryConnectors.find(
const foundInMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === id
);
@ -325,7 +326,7 @@ export class ActionsClient {
);
}
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.UPDATE,
savedObject: { type: 'action', id },
@ -335,11 +336,11 @@ export class ActionsClient {
throw error;
}
const { attributes, references, version } =
await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
await this.context.unsecuredSavedObjectsClient.get<RawAction>('action', id);
const { actionTypeId } = attributes;
const { name, config, secrets } = action;
const actionType = this.actionTypeRegistry.get(actionTypeId);
const configurationUtilities = this.actionTypeRegistry.getUtils();
const actionType = this.context.actionTypeRegistry.get(actionTypeId);
const configurationUtilities = this.context.actionTypeRegistry.getUtils();
const validatedActionTypeConfig = validateConfig(actionType, config, {
configurationUtilities,
});
@ -350,9 +351,9 @@ export class ActionsClient {
validateConnector(actionType, { config, secrets });
}
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
this.context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.UPDATE,
savedObject: { type: 'action', id },
@ -360,7 +361,7 @@ export class ActionsClient {
})
);
const result = await this.unsecuredSavedObjectsClient.create<RawAction>(
const result = await this.context.unsecuredSavedObjectsClient.create<RawAction>(
'action',
{
...attributes,
@ -382,9 +383,9 @@ export class ActionsClient {
);
try {
await this.connectorTokenClient.deleteConnectorTokens({ connectorId: id });
await this.context.connectorTokenClient.deleteConnectorTokens({ connectorId: id });
} catch (e) {
this.logger.error(
this.context.logger.error(
`Failed to delete auth tokens for connector "${id}" after update: ${e.message}`
);
}
@ -412,9 +413,9 @@ export class ActionsClient {
throwIfSystemAction?: boolean;
}): Promise<ActionResult> {
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
await this.context.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET,
savedObject: { type: 'action', id },
@ -424,7 +425,9 @@ export class ActionsClient {
throw error;
}
const foundInMemoryConnector = this.inMemoryConnectors.find((connector) => connector.id === id);
const foundInMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === id
);
/**
* Getting system connector is not allowed
@ -440,7 +443,7 @@ export class ActionsClient {
}
if (foundInMemoryConnector !== undefined) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET,
savedObject: { type: 'action', id },
@ -457,9 +460,9 @@ export class ActionsClient {
};
}
const result = await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
const result = await this.context.unsecuredSavedObjectsClient.get<RawAction>('action', id);
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET,
savedObject: { type: 'action', id },
@ -479,57 +482,10 @@ export class ActionsClient {
}
/**
* Get all actions with in-memory connectors
* Get all connectors with in-memory connectors
*/
public async getAll({ includeSystemActions = false }: GetAllOptions = {}): Promise<
FindActionResult[]
> {
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
this.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.FIND,
error,
})
);
throw error;
}
const savedObjectsActions = (
await this.unsecuredSavedObjectsClient.find<RawAction>({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
})
).saved_objects.map((rawAction) =>
actionFromSavedObject(rawAction, isConnectorDeprecated(rawAction.attributes))
);
savedObjectsActions.forEach(({ id }) =>
this.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.FIND,
savedObject: { type: 'action', id },
})
)
);
const inMemoryConnectorsFiltered = includeSystemActions
? this.inMemoryConnectors
: this.inMemoryConnectors.filter((connector) => !connector.isSystemAction);
const mergedResult = [
...savedObjectsActions,
...inMemoryConnectorsFiltered.map((inMemoryConnector) => ({
id: inMemoryConnector.id,
actionTypeId: inMemoryConnector.actionTypeId,
name: inMemoryConnector.name,
isPreconfigured: inMemoryConnector.isPreconfigured,
isDeprecated: isConnectorDeprecated(inMemoryConnector),
isSystemAction: inMemoryConnector.isSystemAction,
})),
].sort((a, b) => a.name.localeCompare(b.name));
return await injectExtraFindData(this.kibanaIndices, this.scopedClusterClient, mergedResult);
public async getAll({ includeSystemActions = false } = {}): Promise<FindConnectorResult[]> {
return getAll({ context: this.context, includeSystemActions });
}
/**
@ -543,10 +499,10 @@ export class ActionsClient {
throwIfSystemAction?: boolean;
}): Promise<ActionResult[]> {
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
await this.context.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
ids.forEach((id) =>
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET,
savedObject: { type: 'action', id },
@ -560,7 +516,7 @@ export class ActionsClient {
const actionResults = new Array<ActionResult>();
for (const actionId of ids) {
const action = this.inMemoryConnectors.find(
const action = this.context.inMemoryConnectors.find(
(inMemoryConnector) => inMemoryConnector.id === actionId
);
@ -589,11 +545,13 @@ export class ActionsClient {
];
const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' }));
const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet<RawAction>(bulkGetOpts);
const bulkGetResult = await this.context.unsecuredSavedObjectsClient.bulkGet<RawAction>(
bulkGetOpts
);
bulkGetResult.saved_objects.forEach(({ id, error }) => {
if (!error && this.auditLogger) {
this.auditLogger.log(
if (!error && this.context.auditLogger) {
this.context.auditLogger.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET,
savedObject: { type: 'action', id },
@ -608,7 +566,9 @@ export class ActionsClient {
`Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}`
);
}
actionResults.push(actionFromSavedObject(action, isConnectorDeprecated(action.attributes)));
actionResults.push(
connectorFromSavedObject(action, isConnectorDeprecated(action.attributes))
);
}
return actionResults;
@ -619,7 +579,7 @@ export class ActionsClient {
configurationUtilities: ActionsConfigurationUtilities
) {
// Verify that user has edit access
await this.authorization.ensureAuthorized({ operation: 'update' });
await this.context.authorization.ensureAuthorized({ operation: 'update' });
// Verify that token url is allowed by allowed hosts config
try {
@ -649,7 +609,7 @@ export class ActionsClient {
try {
accessToken = await getOAuthJwtAccessToken({
logger: this.logger,
logger: this.context.logger,
configurationUtilities,
credentials: {
config: tokenOpts.config as GetOAuthJwtConfig,
@ -658,13 +618,13 @@ export class ActionsClient {
tokenUrl: tokenOpts.tokenUrl,
});
this.logger.debug(
this.context.logger.debug(
`Successfully retrieved access token using JWT OAuth with tokenUrl ${
tokenOpts.tokenUrl
} and config ${JSON.stringify(tokenOpts.config)}`
);
} catch (err) {
this.logger.debug(
this.context.logger.debug(
`Failed to retrieve access token using JWT OAuth with tokenUrl ${
tokenOpts.tokenUrl
} and config ${JSON.stringify(tokenOpts.config)} - ${err.message}`
@ -675,7 +635,7 @@ export class ActionsClient {
const tokenOpts = options as OAuthClientCredentialsParams;
try {
accessToken = await getOAuthClientCredentialsAccessToken({
logger: this.logger,
logger: this.context.logger,
configurationUtilities,
credentials: {
config: tokenOpts.config as GetOAuthClientCredentialsConfig,
@ -685,13 +645,13 @@ export class ActionsClient {
oAuthScope: tokenOpts.scope,
});
this.logger.debug(
this.context.logger.debug(
`Successfully retrieved access token using Client Credentials OAuth with tokenUrl ${
tokenOpts.tokenUrl
}, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)}`
);
} catch (err) {
this.logger.debug(
this.context.logger.debug(
`Failed to retrieved access token using Client Credentials OAuth with tokenUrl ${
tokenOpts.tokenUrl
}, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)} - ${
@ -710,9 +670,9 @@ export class ActionsClient {
*/
public async delete({ id }: { id: string }) {
try {
await this.authorization.ensureAuthorized({ operation: 'delete' });
await this.context.authorization.ensureAuthorized({ operation: 'delete' });
const foundInMemoryConnector = this.inMemoryConnectors.find(
const foundInMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === id
);
@ -739,7 +699,7 @@ export class ActionsClient {
);
}
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.DELETE,
savedObject: { type: 'action', id },
@ -749,7 +709,7 @@ export class ActionsClient {
throw error;
}
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.DELETE,
outcome: 'unknown',
@ -758,23 +718,23 @@ export class ActionsClient {
);
try {
await this.connectorTokenClient.deleteConnectorTokens({ connectorId: id });
await this.context.connectorTokenClient.deleteConnectorTokens({ connectorId: id });
} catch (e) {
this.logger.error(
this.context.logger.error(
`Failed to delete auth tokens for connector "${id}" after delete: ${e.message}`
);
}
return await this.unsecuredSavedObjectsClient.delete('action', id);
return await this.context.unsecuredSavedObjectsClient.delete('action', id);
}
private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) {
const inMemoryConnector = this.inMemoryConnectors.find(
const inMemoryConnector = this.context.inMemoryConnectors.find(
(connector) => connector.id === connectorId
);
const additionalPrivileges = inMemoryConnector?.isSystemAction
? this.actionTypeRegistry.getSystemActionKibanaPrivileges(
? this.context.actionTypeRegistry.getSystemActionKibanaPrivileges(
inMemoryConnector.actionTypeId,
params
)
@ -792,20 +752,23 @@ export class ActionsClient {
ActionTypeExecutorResult<unknown>
> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params);
await this.authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
await this.context.authorization.ensureAuthorized({
operation: 'execute',
additionalPrivileges,
});
} else {
trackLegacyRBACExemption('execute', this.usageCounter);
trackLegacyRBACExemption('execute', this.context.usageCounter);
}
return this.actionExecutor.execute({
return this.context.actionExecutor.execute({
actionId,
params,
source,
request: this.request,
request: this.context.request,
relatedSavedObjects,
actionExecutionId: uuidv4(),
});
@ -814,7 +777,7 @@ export class ActionsClient {
public async enqueueExecution(options: EnqueueExecutionOptions): Promise<void> {
const { source } = options;
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
/**
@ -823,11 +786,11 @@ export class ActionsClient {
* inside the ActionExecutor at execution time
*/
await this.authorization.ensureAuthorized({ operation: 'execute' });
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
} else {
trackLegacyRBACExemption('enqueueExecution', this.usageCounter);
trackLegacyRBACExemption('enqueueExecution', this.context.usageCounter);
}
return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options);
return this.context.executionEnqueuer(this.context.unsecuredSavedObjectsClient, options);
}
public async bulkEnqueueExecution(options: EnqueueExecutionOptions[]): Promise<void> {
@ -839,7 +802,7 @@ export class ActionsClient {
});
const authCounts = await getBulkAuthorizationModeBySource(
this.unsecuredSavedObjectsClient,
this.context.unsecuredSavedObjectsClient,
sources
);
if (authCounts[AuthorizationMode.RBAC] > 0) {
@ -848,29 +811,32 @@ export class ActionsClient {
* for system actions (kibana privileges) will be performed
* inside the ActionExecutor at execution time
*/
await this.authorization.ensureAuthorized({ operation: 'execute' });
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
}
if (authCounts[AuthorizationMode.Legacy] > 0) {
trackLegacyRBACExemption(
'bulkEnqueueExecution',
this.usageCounter,
this.context.usageCounter,
authCounts[AuthorizationMode.Legacy]
);
}
return this.bulkExecutionEnqueuer(this.unsecuredSavedObjectsClient, options);
return this.context.bulkExecutionEnqueuer(this.context.unsecuredSavedObjectsClient, options);
}
public async ephemeralEnqueuedExecution(options: EnqueueExecutionOptions): Promise<RunNowResult> {
const { source } = options;
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
await this.authorization.ensureAuthorized({ operation: 'execute' });
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
} else {
trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter);
trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.context.usageCounter);
}
return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options);
return this.context.ephemeralExecutionEnqueuer(
this.context.unsecuredSavedObjectsClient,
options
);
}
/**
@ -881,7 +847,7 @@ export class ActionsClient {
featureId,
includeSystemActionTypes = false,
}: ListTypesOptions = {}): Promise<ActionType[]> {
const actionTypes = this.actionTypeRegistry.list(featureId);
const actionTypes = this.context.actionTypeRegistry.list(featureId);
const filteredActionTypes = includeSystemActionTypes
? actionTypes
@ -894,17 +860,17 @@ export class ActionsClient {
actionTypeId: string,
options: { notifyUsage: boolean } = { notifyUsage: false }
) {
return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options);
return this.context.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options);
}
public isPreconfigured(connectorId: string): boolean {
return !!this.inMemoryConnectors.find(
return !!this.context.inMemoryConnectors.find(
(connector) => connector.isPreconfigured && connector.id === connectorId
);
}
public isSystemAction(connectorId: string): boolean {
return !!this.inMemoryConnectors.find(
return !!this.context.inMemoryConnectors.find(
(connector) => connector.isSystemAction && connector.id === connectorId
);
}
@ -918,13 +884,13 @@ export class ActionsClient {
sort,
namespaces,
}: GetGlobalExecutionLogParams): Promise<IExecutionLogResult> {
this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
this.context.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
const authorizationTuple = {} as KueryNode;
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
await this.context.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_LOG,
error,
@ -933,7 +899,7 @@ export class ActionsClient {
throw error;
}
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_LOG,
})
@ -943,7 +909,7 @@ export class ActionsClient {
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
const eventLogClient = await this.getEventLogClient();
const eventLogClient = await this.context.getEventLogClient();
try {
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
@ -965,7 +931,7 @@ export class ActionsClient {
return formatExecutionLogResult(aggResult);
} catch (err) {
this.logger.debug(
this.context.logger.debug(
`actionsClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}`
);
throw err;
@ -978,13 +944,13 @@ export class ActionsClient {
filter,
namespaces,
}: GetGlobalExecutionKPIParams) {
this.logger.debug(`getGlobalExecutionKpiWithAuth(): getting global execution KPI`);
this.context.logger.debug(`getGlobalExecutionKpiWithAuth(): getting global execution KPI`);
const authorizationTuple = {} as KueryNode;
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
await this.context.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_KPI,
error,
@ -993,7 +959,7 @@ export class ActionsClient {
throw error;
}
this.auditLogger?.log(
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_KPI,
})
@ -1003,7 +969,7 @@ export class ActionsClient {
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
const eventLogClient = await this.getEventLogClient();
const eventLogClient = await this.context.getEventLogClient();
try {
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
@ -1022,79 +988,10 @@ export class ActionsClient {
return formatExecutionKPIResult(aggResult);
} catch (err) {
this.logger.debug(
this.context.logger.debug(
`actionsClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}`
);
throw err;
}
}
}
function actionFromSavedObject(
savedObject: SavedObject<RawAction>,
isDeprecated: boolean
): ActionResult {
return {
id: savedObject.id,
...savedObject.attributes,
isPreconfigured: false,
isDeprecated,
isSystemAction: false,
};
}
async function injectExtraFindData(
kibanaIndices: string[],
scopedClusterClient: IScopedClusterClient,
actionResults: ActionResult[]
): Promise<FindActionResult[]> {
const aggs: Record<string, estypes.AggregationsAggregationContainer> = {};
for (const actionResult of actionResults) {
aggs[actionResult.id] = {
filter: {
bool: {
must: {
nested: {
path: 'references',
query: {
bool: {
filter: {
bool: {
must: [
{
term: {
'references.id': actionResult.id,
},
},
{
term: {
'references.type': 'action',
},
},
],
},
},
},
},
},
},
},
},
};
}
const aggregationResult = await scopedClusterClient.asInternalUser.search({
index: kibanaIndices,
body: {
aggs,
size: 0,
query: {
match_all: {},
},
},
});
return actionResults.map((actionResult) => ({
...actionResult,
// @ts-expect-error aggegation type is not specified
referencedByCount: aggregationResult.aggregations[actionResult.id].doc_count,
}));
}

View file

@ -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 './actions_client';

View file

@ -0,0 +1,23 @@
/*
* 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 { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
import { RawAction } from '../../../types';
import { Connector } from '../types';
export function connectorFromSavedObject(
savedObject: SavedObject<RawAction>,
isDeprecated: boolean
): Connector {
return {
id: savedObject.id,
...savedObject.attributes,
isPreconfigured: false,
isDeprecated,
isSystemAction: false,
};
}

View file

@ -0,0 +1,10 @@
/*
* 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 type { ConnectorWithOptionalDeprecation } from './is_connector_deprecated';
export { isConnectorDeprecated } from './is_connector_deprecated';
export { connectorFromSavedObject } from './connector_from_save_object';

View file

@ -6,7 +6,7 @@
*/
import { isPlainObject } from 'lodash';
import { InMemoryConnector, RawAction } from '../types';
import { RawAction, InMemoryConnector } from '../../../types';
export type ConnectorWithOptionalDeprecation = Omit<InMemoryConnector, 'isDeprecated'> &
Pick<Partial<InMemoryConnector>, 'isDeprecated'>;

View file

@ -0,0 +1,559 @@
/*
* 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 { ActionsClient } from '../../../../actions_client';
import { ActionsAuthorization } from '../../../../authorization/actions_authorization';
import { connectorTokenClientMock } from '../../../../lib/connector_token_client.mock';
import { getOAuthJwtAccessToken } from '../../../../lib/get_oauth_jwt_access_token';
import { getOAuthClientCredentialsAccessToken } from '../../../../lib/get_oauth_client_credentials_access_token';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { actionsAuthorizationMock } from '../../../../authorization/actions_authorization.mock';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { actionExecutorMock } from '../../../../lib/action_executor.mock';
import { httpServerMock } from '@kbn/core-http-server-mocks';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Logger } from '@kbn/logging';
import { eventLogClientMock } from '@kbn/event-log-plugin/server/event_log_client.mock';
import { ActionTypeRegistry } from '../../../../action_type_registry';
jest.mock('@kbn/core-saved-objects-utils-server', () => {
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
return {
...actual,
SavedObjectsUtils: {
generateId: () => 'mock-saved-object-id',
},
};
});
jest.mock('../../../../lib/track_legacy_rbac_exemption', () => ({
trackLegacyRBACExemption: jest.fn(),
}));
jest.mock('../../../../authorization/get_authorization_mode_by_source', () => {
return {
getAuthorizationModeBySource: jest.fn(() => {
return 1;
}),
getBulkAuthorizationModeBySource: jest.fn(() => {
return 1;
}),
AuthorizationMode: {
Legacy: 0,
RBAC: 1,
},
};
});
jest.mock('../../../../lib/get_oauth_jwt_access_token', () => ({
getOAuthJwtAccessToken: jest.fn(),
}));
jest.mock('../../../../lib/get_oauth_client_credentials_access_token', () => ({
getOAuthClientCredentialsAccessToken: jest.fn(),
}));
jest.mock('uuid', () => ({
v4: () => 'uuidv4',
}));
const kibanaIndices = ['.kibana'];
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
const actionExecutor = actionExecutorMock.create();
const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
const ephemeralExecutionEnqueuer = jest.fn();
const bulkExecutionEnqueuer = jest.fn();
const request = httpServerMock.createKibanaRequest();
const auditLogger = auditLoggerMock.create();
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const eventLogClient = eventLogClientMock.create();
const getEventLogClient = jest.fn();
const connectorTokenClient = connectorTokenClientMock.create();
let actionsClient: ActionsClient;
let actionTypeRegistry: ActionTypeRegistry;
describe('getAll()', () => {
beforeEach(() => {
jest.resetAllMocks();
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
inMemoryConnectors: [],
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
auditLogger,
usageCounter: mockUsageCounter,
connectorTokenClient,
getEventLogClient,
});
(getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`);
(getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue(
`Bearer clienttokentokentoken`
);
getEventLogClient.mockResolvedValue(eventLogClient);
});
describe('authorization', () => {
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
return actionsClient.getAll();
}
test('ensures user is authorised to get the type of action', async () => {
await getAllOperation();
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
});
test('throws when user is not authorised to create the type of action', async () => {
authorization.ensureAuthorized.mockRejectedValue(
new Error(`Unauthorized to get all actions`)
);
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
`[Error: Unauthorized to get all actions]`
);
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
});
});
describe('auditLogger', () => {
test('logs audit event when searching connectors', async () => {
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
});
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
}
);
await actionsClient.getAll();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'connector_find',
outcome: 'success',
}),
kibana: { saved_object: { id: '1', type: 'action' } },
})
);
});
test('logs audit event when not authorised to search connectors', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(actionsClient.getAll()).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
action: 'connector_find',
outcome: 'failure',
}),
error: { code: 'Error', message: 'Unauthorized' },
})
);
});
});
test('calls unsecuredSavedObjectsClient with parameters and returns inMemoryConnectors correctly', async () => {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
'system-connector-.cases': { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
/**
* System actions will not
* be returned from getAll
* if no options are provided
*/
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
config: {},
secrets: {},
isDeprecated: false,
isMissingSecrets: false,
isPreconfigured: false,
isSystemAction: true,
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
const result = await actionsClient.getAll();
expect(result).toEqual([
{
id: '1',
name: 'test',
isMissingSecrets: false,
config: { foo: 'bar' },
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
referencedByCount: 6,
},
{
id: 'testPreconfigured',
actionTypeId: '.slack',
name: 'test',
isPreconfigured: true,
isSystemAction: false,
isDeprecated: false,
referencedByCount: 2,
},
]);
});
test('get system actions correctly', async () => {
const expectedResult = {
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
};
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
'system-connector-.cases': { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
config: {},
secrets: {},
isDeprecated: false,
isMissingSecrets: false,
isPreconfigured: false,
isSystemAction: true,
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
const result = await actionsClient.getAll({ includeSystemActions: true });
expect(result).toEqual([
{
actionTypeId: '.cases',
id: 'system-connector-.cases',
isDeprecated: false,
isPreconfigured: false,
isSystemAction: true,
name: 'System action: .cases',
referencedByCount: 2,
},
{
id: '1',
name: 'test',
isMissingSecrets: false,
config: { foo: 'bar' },
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
referencedByCount: 6,
},
{
id: 'testPreconfigured',
actionTypeId: '.slack',
name: 'test',
isPreconfigured: true,
isSystemAction: false,
isDeprecated: false,
referencedByCount: 2,
},
]);
});
test('validates connectors before return', async () => {
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'type',
attributes: {
name: 'test',
isMissingSecrets: false,
config: {
foo: 'bar',
},
},
score: 1,
references: [],
},
],
});
scopedClusterClient.asInternalUser.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
}
);
actionsClient = new ActionsClient({
logger,
actionTypeRegistry,
unsecuredSavedObjectsClient,
scopedClusterClient,
kibanaIndices,
actionExecutor,
executionEnqueuer,
ephemeralExecutionEnqueuer,
bulkExecutionEnqueuer,
request,
authorization: authorization as unknown as ActionsAuthorization,
inMemoryConnectors: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'test',
config: {
foo: 'bar',
},
},
],
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
const result = await actionsClient.getAll({ includeSystemActions: true });
expect(result).toEqual([
{
config: {
foo: 'bar',
},
id: '1',
isDeprecated: false,
isMissingSecrets: false,
isPreconfigured: false,
isSystemAction: false,
name: 'test',
referencedByCount: 6,
},
{
actionTypeId: '.slack',
id: 'testPreconfigured',
isDeprecated: false,
isPreconfigured: true,
isSystemAction: false,
name: 'test',
referencedByCount: 2,
},
]);
expect(logger.warn).toHaveBeenCalledWith(
'Error validating connector: 1, Error: [actionTypeId]: expected value of type [string] but got [undefined]'
);
});
});

View file

@ -0,0 +1,129 @@
/*
* 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.
*/
/**
* Get all actions with in-memory connectors
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { connectorSchema } from '../../schemas';
import { findConnectorsSo, searchConnectorsSo } from '../../../../data/connector';
import { GetAllParams, InjectExtraFindDataParams } from './types';
import { ConnectorAuditAction, connectorAuditEvent } from '../../../../lib/audit_events';
import { connectorFromSavedObject, isConnectorDeprecated } from '../../lib';
import { FindConnectorResult } from '../../types';
export async function getAll({
context,
includeSystemActions = false,
}: GetAllParams): Promise<FindConnectorResult[]> {
try {
await context.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {
context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.FIND,
error,
})
);
throw error;
}
const savedObjectsActions = (
await findConnectorsSo({ unsecuredSavedObjectsClient: context.unsecuredSavedObjectsClient })
).saved_objects.map((rawAction) =>
connectorFromSavedObject(rawAction, isConnectorDeprecated(rawAction.attributes))
);
savedObjectsActions.forEach(({ id }) =>
context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.FIND,
savedObject: { type: 'action', id },
})
)
);
const inMemoryConnectorsFiltered = includeSystemActions
? context.inMemoryConnectors
: context.inMemoryConnectors.filter((connector) => !connector.isSystemAction);
const mergedResult = [
...savedObjectsActions,
...inMemoryConnectorsFiltered.map((inMemoryConnector) => ({
id: inMemoryConnector.id,
actionTypeId: inMemoryConnector.actionTypeId,
name: inMemoryConnector.name,
isPreconfigured: inMemoryConnector.isPreconfigured,
isDeprecated: isConnectorDeprecated(inMemoryConnector),
isSystemAction: inMemoryConnector.isSystemAction,
})),
].sort((a, b) => a.name.localeCompare(b.name));
mergedResult.forEach((connector) => {
// Try to validate the connectors, but don't throw.
try {
connectorSchema.validate(connector);
} catch (e) {
context.logger.warn(`Error validating connector: ${connector.id}, ${e}`);
}
});
return await injectExtraFindData({
kibanaIndices: context.kibanaIndices,
scopedClusterClient: context.scopedClusterClient,
connectors: mergedResult,
});
}
async function injectExtraFindData({
kibanaIndices,
scopedClusterClient,
connectors,
}: InjectExtraFindDataParams): Promise<FindConnectorResult[]> {
const aggs: Record<string, estypes.AggregationsAggregationContainer> = {};
for (const connector of connectors) {
aggs[connector.id] = {
filter: {
bool: {
must: {
nested: {
path: 'references',
query: {
bool: {
filter: {
bool: {
must: [
{
term: {
'references.id': connector.id,
},
},
{
term: {
'references.type': 'action',
},
},
],
},
},
},
},
},
},
},
},
};
}
const aggregationResult = await searchConnectorsSo({ scopedClusterClient, kibanaIndices, aggs });
return connectors.map((connector) => ({
...connector,
// @ts-expect-error aggegation type is not specified
referencedByCount: aggregationResult.aggregations[connector.id].doc_count,
}));
}

View file

@ -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 { getAll } from './get_all';

View file

@ -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 type { GetAllParams, InjectExtraFindDataParams } from './params';

View file

@ -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.
*/
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { ActionsClientContext } from '../../../../../actions_client';
import { Connector } from '../../../types';
export interface GetAllParams {
includeSystemActions?: boolean;
context: ActionsClientContext;
}
export interface InjectExtraFindDataParams {
kibanaIndices: string[];
scopedClusterClient: IScopedClusterClient;
connectors: Connector[];
}

View file

@ -0,0 +1,19 @@
/*
* 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 connectorSchema = schema.object({
id: schema.string(),
name: schema.string(),
actionTypeId: schema.string(),
config: schema.maybe(schema.recordOf(schema.string(), schema.any())),
isMissingSecrets: schema.maybe(schema.boolean()),
isPreconfigured: schema.boolean(),
isDeprecated: schema.boolean(),
isSystemAction: schema.boolean(),
});

View file

@ -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 './connector_schema';

View file

@ -0,0 +1,27 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { connectorSchema } from '../schemas';
import { ActionTypeConfig } from '../../../types';
type ConnectorSchemaType = TypeOf<typeof connectorSchema>;
export interface Connector<Config extends ActionTypeConfig = ActionTypeConfig> {
id: ConnectorSchemaType['id'];
actionTypeId: ConnectorSchemaType['actionTypeId'];
name: ConnectorSchemaType['name'];
isMissingSecrets?: ConnectorSchemaType['isMissingSecrets'];
config?: Config;
isPreconfigured: ConnectorSchemaType['isPreconfigured'];
isDeprecated: ConnectorSchemaType['isDeprecated'];
isSystemAction: ConnectorSchemaType['isSystemAction'];
}
export interface FindConnectorResult extends Connector {
referencedByCount: number;
}

View file

@ -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 type { Connector, FindConnectorResult } from './connector';

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
// We'll set this max setting assuming it's never reached.
export const MAX_ACTIONS_RETURNED = 10000;

View file

@ -0,0 +1,18 @@
/*
* 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 { FindConnectorsSoResult, FindConnectorsSoParams } from './types';
import { MAX_ACTIONS_RETURNED } from './constants';
export const findConnectorsSo = async ({
unsecuredSavedObjectsClient,
}: FindConnectorsSoParams): Promise<FindConnectorsSoResult> => {
return unsecuredSavedObjectsClient.find({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
});
};

View file

@ -0,0 +1,9 @@
/*
* 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 { findConnectorsSo } from './find_connectors_so';
export { searchConnectorsSo } from './search_connectors_so';

View file

@ -0,0 +1,25 @@
/*
* 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 { SearchConnectorsSoParams } from './types';
export const searchConnectorsSo = async ({
scopedClusterClient,
kibanaIndices,
aggs,
}: SearchConnectorsSoParams) => {
return scopedClusterClient.asInternalUser.search({
index: kibanaIndices,
body: {
aggs,
size: 0,
query: {
match_all: {},
},
},
});
};

View file

@ -0,0 +1,11 @@
/*
* 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 { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server';
import { RawAction } from '../../../types';
export type FindConnectorsSoResult = SavedObjectsFindResponse<RawAction>;

View file

@ -0,0 +1,9 @@
/*
* 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 type { SearchConnectorsSoParams, FindConnectorsSoParams } from './params';
export type { FindConnectorsSoResult } from './find_connectors_so_result';

View file

@ -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 { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
export interface SearchConnectorsSoParams {
kibanaIndices: string[];
scopedClusterClient: IScopedClusterClient;
aggs: Record<string, estypes.AggregationsAggregationContainer>;
}
export interface FindConnectorsSoParams {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
}

View file

@ -24,9 +24,10 @@ export type {
ActionType,
InMemoryConnector,
ActionsApiRequestHandlerContext,
FindActionResult,
} from './types';
export type { FindConnectorResult as FindActionResult } from './application/connector/types';
export type { PluginSetupContract, PluginStartContract } from './plugin';
export {

View file

@ -237,7 +237,7 @@ describe('Actions Plugin', () => {
* that got set up on start (step 3).
*/
// @ts-expect-error: inMemoryConnectors can be accessed
expect(actionsContextHandler.getActionsClient().inMemoryConnectors).toEqual([
expect(actionsContextHandler.getActionsClient().context.inMemoryConnectors).toEqual([
{
id: 'preconfiguredServerLog',
actionTypeId: '.server-log',

View file

@ -96,7 +96,7 @@ import { InMemoryMetrics, registerClusterCollector, registerNodeCollector } from
import {
isConnectorDeprecated,
ConnectorWithOptionalDeprecation,
} from './lib/is_connector_deprecated';
} from './application/connector/lib';
import { createSubActionConnectorFramework } from './sub_action_framework';
import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types';
import { SubActionConnector } from './sub_action_framework/sub_action_connector';

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { getAllActionRoute } from './get_all';
import { getAllConnectorsRoute } from './get_all';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
import { actionsClientMock } from '../actions_client.mock';
import { verifyAccessAndContext } from './verify_access_and_context';
import { licenseStateMock } from '../../../lib/license_state.mock';
import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { actionsClientMock } from '../../../actions_client.mock';
jest.mock('./verify_access_and_context', () => ({
jest.mock('../../verify_access_and_context', () => ({
verifyAccessAndContext: jest.fn(),
}));
@ -21,12 +21,12 @@ beforeEach(() => {
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
});
describe('getAllActionRoute', () => {
it('get all actions with proper parameters', async () => {
describe('getAllConnectorsRoute', () => {
it('get all connectors with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getAllActionRoute(router, licenseState);
getAllConnectorsRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
@ -50,11 +50,11 @@ describe('getAllActionRoute', () => {
});
});
it('ensures the license allows getting all actions', async () => {
it('ensures the license allows getting all connectors', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getAllActionRoute(router, licenseState);
getAllConnectorsRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
@ -70,7 +70,7 @@ describe('getAllActionRoute', () => {
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
});
it('ensures the license check prevents getting all actions', async () => {
it('ensures the license check prevents getting all connectors', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
@ -78,7 +78,7 @@ describe('getAllActionRoute', () => {
throw new Error('OMG');
});
getAllActionRoute(router, licenseState);
getAllConnectorsRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];

View file

@ -0,0 +1,35 @@
/*
* 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 { IRouter } from '@kbn/core/server';
import { ConnectorResponseV1 } from '../../../../common/routes/connector/response';
import { transformGetAllConnectorsResponseV1 } from './transforms';
import { ActionsRequestHandlerContext } from '../../../types';
import { BASE_ACTION_API_PATH } from '../../../../common';
import { ILicenseState } from '../../../lib';
import { verifyAccessAndContext } from '../../verify_access_and_context';
export const getAllConnectorsRoute = (
router: IRouter<ActionsRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${BASE_ACTION_API_PATH}/connectors`,
validate: {},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const actionsClient = (await context.actions).getActionsClient();
const result = await actionsClient.getAll();
const responseBody: ConnectorResponseV1[] = transformGetAllConnectorsResponseV1(result);
return res.ok({ body: responseBody });
})
)
);
};

View file

@ -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 { getAllConnectorsRoute } from './get_all';

View file

@ -0,0 +1,10 @@
/*
* 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 { transformGetAllConnectorsResponse } from './transform_connectors_response/latest';
export { transformGetAllConnectorsResponse as transformGetAllConnectorsResponseV1 } from './transform_connectors_response/v1';

View file

@ -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 { transformGetAllConnectorsResponse } from './v1';

View file

@ -0,0 +1,37 @@
/*
* 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 { FindConnectorResult } from '../../../../../application/connector/types';
import { ConnectorResponseV1 } from '../../../../../../common/routes/connector/response';
export const transformGetAllConnectorsResponse = (
results: FindConnectorResult[]
): ConnectorResponseV1[] => {
return results.map(
({
id,
name,
config,
actionTypeId,
isPreconfigured,
isDeprecated,
referencedByCount,
isMissingSecrets,
isSystemAction,
}) => ({
id,
name,
config,
connector_type_id: actionTypeId,
is_preconfigured: isPreconfigured,
is_deprecated: isDeprecated,
referenced_by_count: referencedByCount,
is_missing_secrets: isMissingSecrets,
is_system_action: isSystemAction,
})
);
};

View file

@ -1,55 +0,0 @@
/*
* 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 { IRouter } from '@kbn/core/server';
import { ILicenseState } from '../lib';
import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common';
import { ActionsRequestHandlerContext, FindActionResult } from '../types';
import { verifyAccessAndContext } from './verify_access_and_context';
const rewriteBodyRes: RewriteResponseCase<FindActionResult[]> = (results) => {
return results.map(
({
actionTypeId,
isPreconfigured,
isDeprecated,
referencedByCount,
isMissingSecrets,
isSystemAction,
...res
}) => ({
...res,
connector_type_id: actionTypeId,
is_preconfigured: isPreconfigured,
is_deprecated: isDeprecated,
referenced_by_count: referencedByCount,
is_missing_secrets: isMissingSecrets,
is_system_action: isSystemAction,
})
);
};
export const getAllActionRoute = (
router: IRouter<ActionsRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${BASE_ACTION_API_PATH}/connectors`,
validate: {},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const actionsClient = (await context.actions).getActionsClient();
const result = await actionsClient.getAll();
return res.ok({
body: rewriteBodyRes(result),
});
})
)
);
};

View file

@ -7,13 +7,13 @@
import { IRouter } from '@kbn/core/server';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { getAllConnectorsRoute } from './connector/get_all';
import { ILicenseState } from '../lib';
import { ActionsRequestHandlerContext } from '../types';
import { createActionRoute } from './create';
import { deleteActionRoute } from './delete';
import { executeActionRoute } from './execute';
import { getActionRoute } from './get';
import { getAllActionRoute } from './get_all';
import { connectorTypesRoute } from './connector_types';
import { updateActionRoute } from './update';
import { getOAuthAccessToken } from './get_oauth_access_token';
@ -37,7 +37,7 @@ export function defineRoutes(opts: RouteOptions) {
createActionRoute(router, licenseState);
deleteActionRoute(router, licenseState);
getActionRoute(router, licenseState);
getAllActionRoute(router, licenseState);
getAllConnectorsRoute(router, licenseState);
updateActionRoute(router, licenseState);
connectorTypesRoute(router, licenseState);
executeActionRoute(router, licenseState);

View file

@ -35,6 +35,7 @@ export type ActionTypeParams = Record<string, unknown>;
export type ConnectorTokenClientContract = PublicMethodsOf<ConnectorTokenClient>;
import type { ActionExecutionSource } from './lib';
import { Connector, FindConnectorResult } from './application/connector/types';
export type { ActionExecutionSource } from './lib';
export { ActionExecutionSourceType } from './lib';
@ -77,16 +78,7 @@ export interface ActionTypeExecutorOptions<
source?: ActionExecutionSource<unknown>;
}
export interface ActionResult<Config extends ActionTypeConfig = ActionTypeConfig> {
id: string;
actionTypeId: string;
name: string;
isMissingSecrets?: boolean;
config?: Config;
isPreconfigured: boolean;
isDeprecated: boolean;
isSystemAction: boolean;
}
export type ActionResult<Config extends ActionTypeConfig = ActionTypeConfig> = Connector<Config>;
export interface InMemoryConnector<
Config extends ActionTypeConfig = ActionTypeConfig,
@ -96,9 +88,7 @@ export interface InMemoryConnector<
config: Config;
}
export interface FindActionResult extends ActionResult {
referencedByCount: number;
}
export type FindActionResult = FindConnectorResult;
// signature of the action type executor function
export type ExecutorType<

View file

@ -39,6 +39,10 @@
"@kbn/core-saved-objects-api-server",
"@kbn/core-elasticsearch-server",
"@kbn/core-http-router-server-internal",
"@kbn/core-saved-objects-common",
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-logging-server-mocks",
],
"exclude": [
"target/**/*",