mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
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:
parent
0d85af23a4
commit
d65b02cf06
41 changed files with 1314 additions and 730 deletions
|
@ -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';
|
|
@ -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';
|
|
@ -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(),
|
||||
});
|
|
@ -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';
|
|
@ -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'];
|
||||
}
|
|
@ -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']> {
|
|
@ -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,
|
||||
}));
|
||||
}
|
8
x-pack/plugins/actions/server/actions_client/index.ts
Normal file
8
x-pack/plugins/actions/server/actions_client/index.ts
Normal 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';
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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'>;
|
|
@ -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]'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}));
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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[];
|
||||
}
|
|
@ -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(),
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
11
x-pack/plugins/actions/server/data/connector/constants.ts
Normal file
11
x-pack/plugins/actions/server/data/connector/constants.ts
Normal 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;
|
|
@ -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',
|
||||
});
|
||||
};
|
9
x-pack/plugins/actions/server/data/connector/index.ts
Normal file
9
x-pack/plugins/actions/server/data/connector/index.ts
Normal 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';
|
|
@ -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: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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>;
|
|
@ -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';
|
20
x-pack/plugins/actions/server/data/connector/types/params.ts
Normal file
20
x-pack/plugins/actions/server/data/connector/types/params.ts
Normal 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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
|
|
@ -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 });
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue