diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts index 2e264300490f8..2032653712a59 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts @@ -10,7 +10,10 @@ import { ActionsAuthorization } from '../../../../authorization/actions_authoriz 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 { + savedObjectsClientMock, + savedObjectsRepositoryMock, +} 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'; @@ -21,6 +24,7 @@ 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'; +import { getAllUnsecured } from './get_all'; jest.mock('@kbn/core-saved-objects-utils-server', () => { const actual = jest.requireActual('@kbn/core-saved-objects-utils-server'); @@ -77,6 +81,7 @@ const logger = loggingSystemMock.create().get() as jest.Mocked; const eventLogClient = eventLogClientMock.create(); const getEventLogClient = jest.fn(); const connectorTokenClient = connectorTokenClientMock.create(); +const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); let actionsClient: ActionsClient; let actionTypeRegistry: ActionTypeRegistry; @@ -551,3 +556,447 @@ describe('getAll()', () => { ); }); }); + +describe('getAllUnsecured()', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('calls internalSavedObjectRepository 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', + }, + secrets: 'this should not be returned', + }, + score: 1, + references: [], + }, + ], + }; + internalSavedObjectsRepository.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 }, + }, + } + ); + + const result = await getAllUnsecured({ + esClient: scopedClusterClient.asInternalUser, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + /** + * System actions will not + * be returned from getAllUnsecured + */ + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ], + internalSavedObjectsRepository, + kibanaIndices, + logger, + spaceId: 'default', + }); + + 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, + }, + ]); + + expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({ + perPage: 10000, + type: 'action', + }); + + expect(scopedClusterClient.asInternalUser.search).toHaveBeenCalledWith({ + index: kibanaIndices, + ignore_unavailable: true, + body: { + aggs: { + '1': { + filter: { + bool: { + must: { + nested: { + path: 'references', + query: { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'references.id': '1', + }, + }, + { + term: { + 'references.type': 'action', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + testPreconfigured: { + filter: { + bool: { + must: { + nested: { + path: 'references', + query: { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'references.id': 'testPreconfigured', + }, + }, + { + term: { + 'references.type': 'action', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + size: 0, + query: { + match_all: {}, + }, + }, + }); + + expect(auditLogger.log).not.toHaveBeenCalled(); + expect(authorization.ensureAuthorized).not.toHaveBeenCalled(); + }); + + test('passed custom space id if defined', async () => { + const expectedResult = { + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'type', + attributes: { + name: 'test', + isMissingSecrets: false, + config: { + foo: 'bar', + }, + secrets: 'this should not be returned', + }, + score: 1, + references: [], + }, + ], + }; + internalSavedObjectsRepository.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 }, + }, + } + ); + + const result = await getAllUnsecured({ + esClient: scopedClusterClient.asInternalUser, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + /** + * System actions will not + * be returned from getAllUnsecured + */ + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ], + internalSavedObjectsRepository, + kibanaIndices, + logger, + spaceId: 'custom', + }); + + 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, + }, + ]); + + expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({ + perPage: 10000, + type: 'action', + namespaces: ['custom'], + }); + + expect(scopedClusterClient.asInternalUser.search).toHaveBeenCalledWith({ + index: kibanaIndices, + ignore_unavailable: true, + body: { + aggs: { + '1': { + filter: { + bool: { + must: { + nested: { + path: 'references', + query: { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'references.id': '1', + }, + }, + { + term: { + 'references.type': 'action', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + testPreconfigured: { + filter: { + bool: { + must: { + nested: { + path: 'references', + query: { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'references.id': 'testPreconfigured', + }, + }, + { + term: { + 'references.type': 'action', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + size: 0, + query: { + match_all: {}, + }, + }, + }); + + expect(auditLogger.log).not.toHaveBeenCalled(); + expect(authorization.ensureAuthorized).not.toHaveBeenCalled(); + }); + + test('validates connectors before return', async () => { + internalSavedObjectsRepository.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 }, + }, + } + ); + + const result = await getAllUnsecured({ + esClient: scopedClusterClient.asInternalUser, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + internalSavedObjectsRepository, + kibanaIndices, + logger, + spaceId: 'default', + }); + + 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]' + ); + }); +}); diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts index 8d764a9c632e0..9c3b9c13924fd 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts @@ -9,12 +9,28 @@ * Get all actions with in-memory connectors */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { AuditLogger } from '@kbn/security-plugin-types-server'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { omit } from 'lodash'; +import { InMemoryConnector } from '../../../..'; +import { SavedObjectClientForFind } from '../../../../data/connector/types/params'; import { connectorWithExtraFindDataSchema } 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 { ConnectorWithExtraFindData } from '../../types'; +import { GetAllUnsecuredParams } from './types/params'; + +interface GetAllHelperOpts { + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; + inMemoryConnectors: InMemoryConnector[]; + kibanaIndices: string[]; + logger: Logger; + namespace?: string; + savedObjectsClient: SavedObjectClientForFind; +} export async function getAll({ context, @@ -32,28 +48,70 @@ export async function getAll({ throw error; } + return await getAllHelper({ + auditLogger: context.auditLogger, + esClient: context.scopedClusterClient.asInternalUser, + inMemoryConnectors: includeSystemActions + ? context.inMemoryConnectors + : context.inMemoryConnectors.filter((connector) => !connector.isSystemAction), + kibanaIndices: context.kibanaIndices, + logger: context.logger, + savedObjectsClient: context.unsecuredSavedObjectsClient, + }); +} + +export async function getAllUnsecured({ + esClient, + inMemoryConnectors, + internalSavedObjectsRepository, + kibanaIndices, + logger, + spaceId, +}: GetAllUnsecuredParams): Promise { + const namespace = spaceId && spaceId !== 'default' ? spaceId : undefined; + + const connectors = await getAllHelper({ + esClient, + // Unsecured execution does not currently support system actions so we filter them out + inMemoryConnectors: inMemoryConnectors.filter((connector) => !connector.isSystemAction), + kibanaIndices, + logger, + namespace, + savedObjectsClient: internalSavedObjectsRepository, + }); + + return connectors.map((connector) => omit(connector, 'secrets')); +} + +async function getAllHelper({ + auditLogger, + esClient, + inMemoryConnectors, + kibanaIndices, + logger, + namespace, + savedObjectsClient, +}: GetAllHelperOpts): Promise { const savedObjectsActions = ( - await findConnectorsSo({ unsecuredSavedObjectsClient: context.unsecuredSavedObjectsClient }) + await findConnectorsSo({ savedObjectsClient, namespace }) ).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); + if (auditLogger) { + savedObjectsActions.forEach(({ id }) => + auditLogger.log( + connectorAuditEvent({ + action: ConnectorAuditAction.FIND, + savedObject: { type: 'action', id }, + }) + ) + ); + } const mergedResult = [ ...savedObjectsActions, - ...inMemoryConnectorsFiltered.map((inMemoryConnector) => ({ + ...inMemoryConnectors.map((inMemoryConnector) => ({ id: inMemoryConnector.id, actionTypeId: inMemoryConnector.actionTypeId, name: inMemoryConnector.name, @@ -64,8 +122,8 @@ export async function getAll({ ].sort((a, b) => a.name.localeCompare(b.name)); const connectors = await injectExtraFindData({ - kibanaIndices: context.kibanaIndices, - scopedClusterClient: context.scopedClusterClient, + kibanaIndices, + esClient, connectors: mergedResult, }); @@ -74,7 +132,7 @@ export async function getAll({ try { connectorWithExtraFindDataSchema.validate(connector); } catch (e) { - context.logger.warn(`Error validating connector: ${connector.id}, ${e}`); + logger.warn(`Error validating connector: ${connector.id}, ${e}`); } }); @@ -83,7 +141,7 @@ export async function getAll({ async function injectExtraFindData({ kibanaIndices, - scopedClusterClient, + esClient, connectors, }: InjectExtraFindDataParams): Promise { const aggs: Record = {}; @@ -121,7 +179,7 @@ async function injectExtraFindData({ }; } - const aggregationResult = await searchConnectorsSo({ scopedClusterClient, kibanaIndices, aggs }); + const aggregationResult = await searchConnectorsSo({ esClient, kibanaIndices, aggs }); return connectors.map((connector) => ({ ...connector, diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts index dcbc4c6fbc957..5b3da65578d65 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { getAll } from './get_all'; +export { getAll, getAllUnsecured } from './get_all'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts index 4e5157a1fdce0..ca0afdb782f7d 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { ISavedObjectsRepository, Logger } from '@kbn/core/server'; +import { AuditLogger } from '@kbn/security-plugin/server'; +import { InMemoryConnector } from '../../../../..'; import { ActionsClientContext } from '../../../../../actions_client'; import { Connector } from '../../../types'; @@ -14,8 +17,18 @@ export interface GetAllParams { context: ActionsClientContext; } +export interface GetAllUnsecuredParams { + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; + inMemoryConnectors: InMemoryConnector[]; + internalSavedObjectsRepository: ISavedObjectsRepository; + kibanaIndices: string[]; + logger: Logger; + spaceId: string; +} + export interface InjectExtraFindDataParams { kibanaIndices: string[]; - scopedClusterClient: IScopedClusterClient; + esClient: ElasticsearchClient; connectors: Connector[]; } diff --git a/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts b/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts index da232f5b2aa83..238ae18a1b62b 100644 --- a/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts +++ b/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts @@ -9,10 +9,12 @@ import { FindConnectorsSoResult, FindConnectorsSoParams } from './types'; import { MAX_ACTIONS_RETURNED } from './constants'; export const findConnectorsSo = async ({ - unsecuredSavedObjectsClient, + savedObjectsClient, + namespace, }: FindConnectorsSoParams): Promise => { - return unsecuredSavedObjectsClient.find({ + return savedObjectsClient.find({ perPage: MAX_ACTIONS_RETURNED, type: 'action', + ...(namespace ? { namespaces: [namespace] } : {}), }); }; diff --git a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts index 09d3ae3b532d9..ab549899348ae 100644 --- a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts +++ b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts @@ -8,11 +8,11 @@ import { SearchConnectorsSoParams } from './types'; export const searchConnectorsSo = async ({ - scopedClusterClient, + esClient, kibanaIndices, aggs, }: SearchConnectorsSoParams) => { - return scopedClusterClient.asInternalUser.search({ + return esClient.search({ index: kibanaIndices, ignore_unavailable: true, body: { diff --git a/x-pack/plugins/actions/server/data/connector/types/params.ts b/x-pack/plugins/actions/server/data/connector/types/params.ts index 73d8ea6dadd14..c23447fb37486 100644 --- a/x-pack/plugins/actions/server/data/connector/types/params.ts +++ b/x-pack/plugins/actions/server/data/connector/types/params.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { SavedObjectsClient } from '@kbn/core/server'; +export type SavedObjectClientForFind = Pick; export interface SearchConnectorsSoParams { kibanaIndices: string[]; - scopedClusterClient: IScopedClusterClient; + esClient: ElasticsearchClient; aggs: Record; } export interface FindConnectorsSoParams { - unsecuredSavedObjectsClient: SavedObjectsClientContract; + savedObjectsClient: SavedObjectClientForFind; + namespace?: string; } export interface GetConnectorSoParams { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 33383e526e36d..1ef28b10e6440 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -486,18 +486,22 @@ export class ActionsPlugin implements Plugin { const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([ + ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, ]); return new UnsecuredActionsClient({ actionExecutor: actionExecutor!, - internalSavedObjectsRepository, + clusterClient: core.elasticsearch.client, executionEnqueuer: createBulkUnsecuredExecutionEnqueuerFunction({ taskManager: plugins.taskManager, connectorTypeRegistry: actionTypeRegistry!, inMemoryConnectors: this.inMemoryConnectors, configurationUtilities: actionsConfigUtils, }), + inMemoryConnectors: this.inMemoryConnectors, + internalSavedObjectsRepository, + kibanaIndices: core.savedObjects.getAllIndices(), logger: this.logger, }); }; diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts index 4cbbfa1604dc1..748847d579eeb 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts @@ -11,6 +11,7 @@ export type UnsecuredActionsClientMock = jest.Mocked; const createUnsecuredActionsClientMock = () => { const mocked: UnsecuredActionsClientMock = { + getAll: jest.fn(), execute: jest.fn(), bulkEnqueueExecution: jest.fn(), }; diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts index 5df39e28fcbc1..89145d80eea19 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts @@ -6,28 +6,125 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsRepositoryMock, +} from '@kbn/core/server/mocks'; import { asNotificationExecutionSource } from '../lib'; import { actionExecutorMock } from '../lib/action_executor.mock'; import { UnsecuredActionsClient } from './unsecured_actions_client'; +import { Logger } from '@kbn/core/server'; +import { getAllUnsecured } from '../application/connector/methods/get_all/get_all'; + +jest.mock('../application/connector/methods/get_all/get_all'); + +const mockGetAllUnsecured = getAllUnsecured as jest.MockedFunction; const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); const actionExecutor = actionExecutorMock.create(); const executionEnqueuer = jest.fn(); -const logger = loggingSystemMock.create().get(); - +const logger = loggingSystemMock.create().get() as jest.Mocked; +const clusterClient = elasticsearchServiceMock.createClusterClient(); +const inMemoryConnectors = [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + /** + * System actions will not + * be returned from getAllUnsecured + */ + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, +]; let unsecuredActionsClient: UnsecuredActionsClient; beforeEach(() => { jest.resetAllMocks(); unsecuredActionsClient = new UnsecuredActionsClient({ actionExecutor, - internalSavedObjectsRepository, + clusterClient, executionEnqueuer, + inMemoryConnectors, + internalSavedObjectsRepository, + kibanaIndices: ['.kibana'], logger, }); }); +describe('getAll()', () => { + test('calls getAllUnsecured library method with appropriate parameters', async () => { + const expectedResult = [ + { + actionTypeId: 'test', + 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, + }, + ]; + mockGetAllUnsecured.mockResolvedValueOnce(expectedResult); + const result = await unsecuredActionsClient.getAll('default'); + expect(result).toEqual(expectedResult); + expect(mockGetAllUnsecured).toHaveBeenCalledWith({ + esClient: clusterClient.asInternalUser, + inMemoryConnectors, + kibanaIndices: ['.kibana'], + logger, + internalSavedObjectsRepository, + spaceId: 'default', + }); + }); + + test('throws error if getAllUnsecured throws errors', async () => { + mockGetAllUnsecured.mockImplementationOnce(() => { + throw new Error('failfail'); + }); + await expect( + unsecuredActionsClient.getAll('customSpace') + ).rejects.toThrowErrorMatchingInlineSnapshot(`"failfail"`); + expect(mockGetAllUnsecured).toHaveBeenCalledWith({ + esClient: clusterClient.asInternalUser, + inMemoryConnectors, + kibanaIndices: ['.kibana'], + logger, + internalSavedObjectsRepository, + spaceId: 'customSpace', + }); + }); +}); + describe('execute()', () => { test('throws error when executing action with not allowed requester id', async () => { await expect( diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts index 96449380a82cd..8331f6890486c 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -6,7 +6,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { ISavedObjectsRepository, Logger } from '@kbn/core/server'; +import { IClusterClient, ISavedObjectsRepository, Logger } from '@kbn/core/server'; import { BulkUnsecuredExecutionEnqueuer, ExecuteOptions, @@ -17,8 +17,10 @@ import { asNotificationExecutionSource, type RelatedSavedObjects, } from '../lib'; -import { ActionTypeExecutorResult } from '../types'; +import { ActionTypeExecutorResult, InMemoryConnector } from '../types'; import { asBackgroundTaskExecutionSource } from '../lib/action_execution_source'; +import { ConnectorWithExtraFindData } from '../application/connector/types'; +import { getAllUnsecured } from '../application/connector/methods/get_all/get_all'; // requests from the notification service (for system notification) const NOTIFICATION_REQUESTER_ID = 'notifications'; @@ -37,8 +39,11 @@ const ALLOWED_REQUESTER_IDS = [ export interface UnsecuredActionsClientOpts { actionExecutor: ActionExecutorContract; - internalSavedObjectsRepository: ISavedObjectsRepository; + clusterClient: IClusterClient; executionEnqueuer: BulkUnsecuredExecutionEnqueuer; + inMemoryConnectors: InMemoryConnector[]; + internalSavedObjectsRepository: ISavedObjectsRepository; + kibanaIndices: string[]; logger: Logger; } @@ -48,6 +53,7 @@ type UnsecuredExecuteOptions = Omit & { }; export interface IUnsecuredActionsClient { + getAll: (spaceId: string) => Promise; execute: (opts: UnsecuredExecuteOptions) => Promise>; bulkEnqueueExecution: ( requesterId: string, @@ -56,17 +62,7 @@ export interface IUnsecuredActionsClient { } export class UnsecuredActionsClient { - private readonly actionExecutor: ActionExecutorContract; - private readonly internalSavedObjectsRepository: ISavedObjectsRepository; - private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer; - private readonly logger: Logger; - - constructor(params: UnsecuredActionsClientOpts) { - this.actionExecutor = params.actionExecutor; - this.executionEnqueuer = params.executionEnqueuer; - this.internalSavedObjectsRepository = params.internalSavedObjectsRepository; - this.logger = params.logger; - } + constructor(private readonly opts: UnsecuredActionsClientOpts) {} public async execute({ requesterId, @@ -83,14 +79,14 @@ export class UnsecuredActionsClient { } if (!relatedSavedObjects) { - this.logger.warn( + this.opts.logger.warn( `Calling "execute" in UnsecuredActionsClient without any relatedSavedObjects data. Consider including this for traceability.` ); } const source = this.getSourceFromRequester(requesterId, id, relatedSavedObjects); - return this.actionExecutor.executeUnsecured({ + return this.opts.actionExecutor.executeUnsecured({ actionExecutionId: uuidv4(), actionId: id, params, @@ -123,7 +119,18 @@ export class UnsecuredActionsClient { ...source, }; }); - return this.executionEnqueuer(this.internalSavedObjectsRepository, actionsToEnqueue); + return this.opts.executionEnqueuer(this.opts.internalSavedObjectsRepository, actionsToEnqueue); + } + + public async getAll(spaceId: string): Promise { + return getAllUnsecured({ + esClient: this.opts.clusterClient.asInternalUser, + inMemoryConnectors: this.opts.inMemoryConnectors, + kibanaIndices: this.opts.kibanaIndices, + logger: this.opts.logger, + internalSavedObjectsRepository: this.opts.internalSavedObjectsRepository, + spaceId, + }); } private getSourceFromRequester( diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index d30c8bdde7d60..aae2d31c7aa09 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -44,7 +44,8 @@ "@kbn/core-logging-server-mocks", "@kbn/serverless", "@kbn/actions-types", - "@kbn/core-test-helpers-kbn-server" + "@kbn/core-test-helpers-kbn-server", + "@kbn/security-plugin-types-server" ], "exclude": [ "target/**/*", diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts index 675dbe50afd54..536abe2d3526e 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts @@ -94,4 +94,33 @@ export function initPlugin(router: IRouter, coreSetup: CoreSetup, + res: KibanaResponseFactory + ): Promise> { + const [_, { actions }] = await coreSetup.getStartServices(); + const { body } = req; + + try { + const unsecuredActionsClient = actions.getUnsecuredActionsClient(); + const { spaceId } = body; + const result = await unsecuredActionsClient.getAll(spaceId); + + return res.ok({ body: { status: 'success', result } }); + } catch (err) { + return res.ok({ body: { status: 'error', error: `${err}` } }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts new file mode 100644 index 0000000000000..3183f19771f16 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts @@ -0,0 +1,209 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; +import { Spaces } from '../../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function createUnsecuredActionTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + const preconfiguredConnectors = [ + { + id: 'preconfigured-alert-history-es-index', + actionTypeId: '.index', + name: 'Alert history Elasticsearch index', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'notification-email', + actionTypeId: '.email', + name: 'Notification Email Connector', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'preconfigured-es-index-action', + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'my-deprecated-servicenow', + actionTypeId: '.servicenow', + name: 'ServiceNow#xyz', + isPreconfigured: true, + isDeprecated: true, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'my-deprecated-servicenow-default', + actionTypeId: '.servicenow', + name: 'ServiceNow#xyz', + isPreconfigured: true, + isDeprecated: true, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'my-slack1', + actionTypeId: '.slack', + name: 'Slack#xyz', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'preconfigured.test.index-record', + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + { + id: 'my-test-email', + actionTypeId: '.email', + name: 'TestEmail#xyz', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + referencedByCount: 0, + }, + ]; + + describe('get all unsecured actions', () => { + const objectRemover = new ObjectRemover(supertest); + + // need to wait for kibanaServer to settle ... + before(() => { + kibanaServer.resolveUrl(`/api/get_all_unsecured_actions`); + }); + + after(() => objectRemover.removeAll()); + + it('should successfully get all actions', async () => { + // Create a connector + const { body: createdConnector1 } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'zzz - My action1', + connector_type_id: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdConnector1.id, 'action', 'actions'); + + const { body: createdConnector2 } = await supertest + .post(`${getUrlPrefix(Spaces.other.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'zzz - My action2', + connector_type_id: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.other.id, createdConnector2.id, 'action', 'actions'); + + const space1SpaceResponse = await supertest + .post(`/api/get_all_unsecured_actions`) + .set('kbn-xsrf', 'xxx') + .send({ + spaceId: Spaces.space1.id, + }) + .expect(200); + expect(space1SpaceResponse.body.status).to.eql('success'); + + // the custom ssl connectors have dynamic ports, so remove them before + // comparing to what we expect + const preconfiguredWithSpace1Connector = space1SpaceResponse.body.result.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') + ); + expect(preconfiguredWithSpace1Connector).to.eql([ + ...preconfiguredConnectors, + { + id: createdConnector1.id, + isPreconfigured: false, + isDeprecated: false, + name: 'zzz - My action1', + actionTypeId: 'test.index-record', + isMissingSecrets: false, + isSystemAction: false, + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + referencedByCount: 0, + }, + ]); + + const otherSpaceResponse = await supertest + .post(`/api/get_all_unsecured_actions`) + .set('kbn-xsrf', 'xxx') + .send({ + spaceId: Spaces.other.id, + }) + .expect(200); + expect(otherSpaceResponse.body.status).to.eql('success'); + + // the custom ssl connectors have dynamic ports, so remove them before + // comparing to what we expect + const preconfiguredWithOtherSpaceConnector = otherSpaceResponse.body.result.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') + ); + expect(preconfiguredWithOtherSpaceConnector).to.eql([ + ...preconfiguredConnectors, + { + id: createdConnector2.id, + isPreconfigured: false, + isDeprecated: false, + name: 'zzz - My action2', + actionTypeId: 'test.index-record', + isMissingSecrets: false, + isSystemAction: false, + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + referencedByCount: 0, + }, + ]); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index 89f1d48285ae2..4f5832debebda 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -31,6 +31,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./type_not_enabled')); loadTestFile(require.resolve('./schedule_unsecured_action')); loadTestFile(require.resolve('./execute_unsecured_action')); + loadTestFile(require.resolve('./get_all_unsecured_actions')); loadTestFile(require.resolve('./check_registered_connector_types')); loadTestFile(require.resolve('./max_queued_actions_circuit_breaker'));