diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx index c9c514a4f4254a..010b4be5ad4b0f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx @@ -50,6 +50,7 @@ export const ConnectorSelector: React.FC = React.memo( enabledInLicense: true, minimumLicenseRequired: 'platinum', supportedFeatureIds: ['general'], + isSystemActionType: false, id: '.gen-ai', name: 'Generative AI', enabled: true, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx index 66c58e2b9ac61f..c430355051ce0f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx @@ -92,6 +92,7 @@ export const useConnectorSetup = ({ actionTypes?.find((at) => at.id === GEN_AI_CONNECTOR_ID) ?? { enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, minimumLicenseRequired: 'platinum', supportedFeatureIds: ['general'], id: '.gen-ai', diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 275a757597d281..c3e1b35777fff3 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -22,6 +22,7 @@ export interface ActionType { enabledInLicense: boolean; minimumLicenseRequired: LicenseType; supportedFeatureIds: string[]; + isSystemActionType: boolean; } export enum InvalidEmailReason { diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 922fc39ada8a26..532b192001e7a8 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -17,6 +17,7 @@ const createActionTypeRegistryMock = () => { ensureActionTypeEnabled: jest.fn(), isActionTypeEnabled: jest.fn(), isActionExecutable: jest.fn(), + isSystemActionType: jest.fn(), getUtils: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 595461b137b593..6988307b01b7e0 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -36,7 +36,7 @@ describe('actionTypeRegistry', () => { ), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, - preconfiguredActions: [ + inMemoryConnectors: [ { actionTypeId: 'foo', config: {}, @@ -47,6 +47,16 @@ describe('actionTypeRegistry', () => { isDeprecated: false, isSystemAction: false, }, + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, ], }; }); @@ -217,7 +227,7 @@ describe('actionTypeRegistry', () => { expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled(); }); - test('does not allows registering system actions', () => { + test('allows registering system actions', () => { const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); expect(() => @@ -226,7 +236,7 @@ describe('actionTypeRegistry', () => { name: 'My action type', minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], - isSystemAction: true, + isSystemActionType: true, validate: { config: { schema: schema.object({}) }, secrets: { schema: schema.object({}) }, @@ -234,7 +244,7 @@ describe('actionTypeRegistry', () => { }, executor, }) - ).toThrowErrorMatchingInlineSnapshot(`"System actions are not supported"`); + ).not.toThrow(); }); }); @@ -302,6 +312,7 @@ describe('actionTypeRegistry', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]); expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalled(); @@ -345,11 +356,46 @@ describe('actionTypeRegistry', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]); expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalled(); expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalled(); }); + + test('sets the isSystemActionType correctly for system actions', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + + actionTypeRegistry.register({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + const actionTypes = actionTypeRegistry.list(); + + expect(actionTypes).toEqual([ + { + id: '.cases', + name: 'Cases', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + isSystemActionType: true, + }, + ]); + }); }); describe('has()', () => { @@ -378,6 +424,7 @@ describe('actionTypeRegistry', () => { describe('isActionTypeEnabled', () => { let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -393,9 +440,17 @@ describe('actionTypeRegistry', () => { }, }; + const systemActionType: ActionType = { + ...fooActionType, + id: 'system-action-type', + name: 'System action type', + isSystemActionType: true, + }; + beforeEach(() => { actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register(fooActionType); + actionTypeRegistry.register(systemActionType); }); test('should call isActionTypeEnabled of the actions config', async () => { @@ -417,6 +472,15 @@ describe('actionTypeRegistry', () => { expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); }); + test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has system connectors', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + expect( + actionTypeRegistry.isActionExecutable('system-connector-.cases', 'system-action-type') + ).toEqual(true); + }); + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); @@ -567,4 +631,62 @@ describe('actionTypeRegistry', () => { expect(result).toEqual(['foo']); }); }); + + describe('isSystemActionType()', () => { + it('should return true if the action type is a system action type', () => { + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + registry.register({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + const result = registry.isSystemActionType('.cases'); + expect(result).toBe(true); + }); + + it('should return false if the action type is not a system action type', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + registry.register({ + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + const allTypes = registry.getAllTypes(); + expect(allTypes.length).toBe(1); + + const result = registry.isSystemActionType('foo'); + expect(result).toBe(false); + }); + + it('should return false if the action type does not exists', () => { + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + const allTypes = registry.getAllTypes(); + expect(allTypes.length).toBe(0); + + const result = registry.isSystemActionType('not-exist'); + expect(result).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 8404bafbd8994f..1b133af7584b68 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -14,7 +14,7 @@ import { ActionsConfigurationUtilities } from './actions_config'; import { getActionTypeFeatureUsageName, TaskRunnerFactory, ILicenseState } from './lib'; import { ActionType, - PreConfiguredAction, + InMemoryConnector, ActionTypeConfig, ActionTypeSecrets, ActionTypeParams, @@ -26,7 +26,7 @@ export interface ActionTypeRegistryOpts { taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; licenseState: ILicenseState; - preconfiguredActions: PreConfiguredAction[]; + inMemoryConnectors: InMemoryConnector[]; } export class ActionTypeRegistry { @@ -35,7 +35,7 @@ export class ActionTypeRegistry { private readonly taskRunnerFactory: TaskRunnerFactory; private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; - private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly inMemoryConnectors: InMemoryConnector[]; private readonly licensing: LicensingPluginSetup; constructor(constructorParams: ActionTypeRegistryOpts) { @@ -43,7 +43,7 @@ export class ActionTypeRegistry { this.taskRunnerFactory = constructorParams.taskRunnerFactory; this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; - this.preconfiguredActions = constructorParams.preconfiguredActions; + this.inMemoryConnectors = constructorParams.inMemoryConnectors; this.licensing = constructorParams.licensing; } @@ -78,7 +78,7 @@ export class ActionTypeRegistry { } /** - * Returns true if action type is enabled or it is a preconfigured action type. + * Returns true if action type is enabled or it is an in memory action type. */ public isActionExecutable( actionId: string, @@ -89,12 +89,17 @@ export class ActionTypeRegistry { return ( actionTypeEnabled || (!actionTypeEnabled && - this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === actionId - ) !== undefined) + this.inMemoryConnectors.find((inMemoryConnector) => inMemoryConnector.id === actionId) !== + undefined) ); } + /** + * Returns true if the action type is a system action type + */ + public isSystemActionType = (actionTypeId: string): boolean => + Boolean(this.actionTypes.get(actionTypeId)?.isSystemActionType); + /** * Registers an action type to the action type registry */ @@ -104,18 +109,6 @@ export class ActionTypeRegistry { Params extends ActionTypeParams = ActionTypeParams, ExecutorResultData = void >(actionType: ActionType) { - // TODO: Remove when system action are supported - if (actionType.isSystemAction) { - throw new Error( - i18n.translate( - 'xpack.actions.actionTypeRegistry.register.systemActionsNotSupportedErrorMessage', - { - defaultMessage: 'System actions are not supported', - } - ) - ); - } - if (this.has(actionType.id)) { throw new Error( i18n.translate( @@ -214,6 +207,7 @@ export class ActionTypeRegistry { enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid, supportedFeatureIds: actionType.supportedFeatureIds, + isSystemActionType: !!actionType.isSystemActionType, })); } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index b80914c309b253..3fb08e50087939 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -27,6 +27,7 @@ const createActionsClientMock = () => { listTypes: jest.fn(), isActionTypeEnabled: jest.fn(), isPreconfigured: jest.fn(), + isSystemAction: jest.fn(), getGlobalExecutionKpiWithAuth: jest.fn(), getGlobalExecutionLogWithAuth: jest.fn(), }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 14a295cfec325b..a18a8adada424d 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -133,7 +133,7 @@ beforeEach(() => { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, - preconfiguredActions: [], + inMemoryConnectors: [], }; actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ @@ -142,7 +142,7 @@ beforeEach(() => { unsecuredSavedObjectsClient, scopedClusterClient, kibanaIndices, - preconfiguredActions: [], + inMemoryConnectors: [], actionExecutor, executionEnqueuer, ephemeralExecutionEnqueuer, @@ -594,7 +594,7 @@ describe('create()', () => { ), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }; actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); @@ -604,7 +604,7 @@ describe('create()', () => { unsecuredSavedObjectsClient, scopedClusterClient, kibanaIndices, - preconfiguredActions: [], + inMemoryConnectors: [], actionExecutor, executionEnqueuer, ephemeralExecutionEnqueuer, @@ -715,7 +715,7 @@ describe('create()', () => { unsecuredSavedObjectsClient, scopedClusterClient, kibanaIndices, - preconfiguredActions: [ + inMemoryConnectors: [ { id: preDefinedId, actionTypeId: 'my-action-type', @@ -755,9 +755,79 @@ describe('create()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"This mySuperRadTestPreconfiguredId already exist in preconfigured action."` + `"This mySuperRadTestPreconfiguredId already exists in a preconfigured action."` ); }); + + it('throws when creating a system connector', async () => { + actionTypeRegistry.register({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + await expect( + actionsClient.create({ + action: { + name: 'my name', + actionTypeId: '.cases', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"System action creation is forbidden. Action type: .cases."` + ); + }); + + it('throws when creating a system connector where the action type is not registered but a system connector exists in the in-memory list', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await expect( + actionsClient.create({ + action: { + name: 'my name', + actionTypeId: '.cases', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Action type \\".cases\\" is not registered."`); + }); }); describe('get()', () => { @@ -793,7 +863,7 @@ describe('get()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: 'my-action-type', @@ -818,6 +888,40 @@ describe('get()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); }); + test('ensures user is authorised to get a system action', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await actionsClient.get({ id: 'system-connector-.cases' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + test('throws when user is not authorised to get the type of action', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -855,7 +959,7 @@ describe('get()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: 'my-action-type', @@ -885,6 +989,48 @@ describe('get()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); }); + + test('throws when user is not authorised to get a system action', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "system-connector-.cases" action`) + ); + + await expect( + actionsClient.get({ id: 'system-connector-.cases' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "system-connector-.cases" action]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); }); describe('auditLogger', () => { @@ -980,7 +1126,7 @@ describe('get()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1011,6 +1157,50 @@ describe('get()', () => { }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); }); + + it('return system action with id', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + 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.get({ id: 'system-connector-.cases' }); + + expect(result).toEqual({ + id: 'system-connector-.cases', + actionTypeId: '.cases', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + name: 'System action: .cases', + }); + + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + }); }); describe('getAll()', () => { @@ -1058,7 +1248,7 @@ describe('getAll()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1158,7 +1348,7 @@ describe('getAll()', () => { }); }); - test('calls unsecuredSavedObjectsClient with parameters', async () => { + test('calls unsecuredSavedObjectsClient with parameters and returns inMemoryConnectors correctly', async () => { const expectedResult = { total: 1, per_page: 10, @@ -1186,6 +1376,7 @@ describe('getAll()', () => { aggregations: { '1': { doc_count: 6 }, testPreconfigured: { doc_count: 2 }, + 'system-connector-.cases': { doc_count: 2 }, }, } ); @@ -1202,7 +1393,7 @@ describe('getAll()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1215,31 +1406,51 @@ describe('getAll()', () => { 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(); + expect(result).toEqual([ { - id: '1', + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', isPreconfigured: false, - isSystemAction: false, + isSystemAction: true, isDeprecated: false, + referencedByCount: 2, + }, + { + id: '1', name: 'test', - config: { - foo: 'bar', - }, 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, - name: 'test', referencedByCount: 2, }, ]); @@ -1288,7 +1499,7 @@ describe('getBulk()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1386,7 +1597,7 @@ describe('getBulk()', () => { }); }); - test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => { + test('calls getBulk unsecuredSavedObjectsClient with parameters and return inMemoryConnectors correctly', async () => { unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -1410,6 +1621,7 @@ describe('getBulk()', () => { aggregations: { '1': { doc_count: 6 }, testPreconfigured: { doc_count: 2 }, + 'system-connector-.cases': { doc_count: 2 }, }, } ); @@ -1426,7 +1638,7 @@ describe('getBulk()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1439,35 +1651,59 @@ describe('getBulk()', () => { 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.getBulk(['1', 'testPreconfigured']); + + const result = await actionsClient.getBulk([ + '1', + 'testPreconfigured', + 'system-connector-.cases', + ]); + expect(result).toEqual([ { - actionTypeId: '.slack', - config: { - foo: 'bar', - }, id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, isPreconfigured: true, isSystemAction: false, isDeprecated: 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, }, { - actionTypeId: 'test', - config: { - foo: 'bar', - }, id: '1', + actionTypeId: 'test', + name: 'test', + config: { foo: 'bar' }, isMissingSecrets: false, isPreconfigured: false, isSystemAction: false, isDeprecated: false, - name: 'test', }, ]); }); @@ -1489,7 +1725,7 @@ describe('getOAuthAccessToken()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: '.slack', @@ -1842,6 +2078,83 @@ describe('delete()', () => { ] `); }); + + it('throws when trying to delete a preconfigured connector', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await expect( + actionsClient.delete({ id: 'testPreconfigured' }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Preconfigured action testPreconfigured is not allowed to delete."` + ); + }); + + it('throws when trying to delete a system connector', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + inMemoryConnectors: [ + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ], + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await expect( + actionsClient.delete({ id: 'system-connector-.cases' }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"System action system-connector-.cases is not allowed to delete."` + ); + }); }); describe('update()', () => { @@ -2318,6 +2631,97 @@ describe('update()', () => { }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); + + it('throws when trying to update a preconfigured connector', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await expect( + actionsClient.update({ + id: 'testPreconfigured', + action: { + name: 'my name', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Preconfigured action testPreconfigured can not be updated."` + ); + }); + + it('throws when trying to update a system connector', async () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + inMemoryConnectors: [ + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ], + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + connectorTokenClient: connectorTokenClientMock.create(), + getEventLogClient, + }); + + await expect( + actionsClient.update({ + id: 'system-connector-.cases', + action: { + name: 'my name', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"System action system-connector-.cases can not be updated."` + ); + }); }); describe('execute()', () => { @@ -2701,7 +3105,7 @@ describe('isActionTypeEnabled()', () => { }); describe('isPreconfigured()', () => { - test('should return true if connector id is in list of preconfigured connectors', () => { + test('should return true if the connector is a preconfigured connector', () => { actionsClient = new ActionsClient({ logger, actionTypeRegistry, @@ -2714,7 +3118,7 @@ describe('isPreconfigured()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: 'my-action-type', @@ -2729,6 +3133,17 @@ describe('isPreconfigured()', () => { foo: 'bar', }, }, + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, ], connectorTokenClient: new ConnectorTokenClient({ unsecuredSavedObjectsClient: savedObjectsClientMock.create(), @@ -2741,7 +3156,7 @@ describe('isPreconfigured()', () => { expect(actionsClient.isPreconfigured('testPreconfigured')).toEqual(true); }); - test('should return false if connector id is not in list of preconfigured connectors', () => { + test('should return false if the connector is not preconfigured connector', () => { actionsClient = new ActionsClient({ logger, actionTypeRegistry, @@ -2754,7 +3169,7 @@ describe('isPreconfigured()', () => { bulkExecutionEnqueuer, request, authorization: authorization as unknown as ActionsAuthorization, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'testPreconfigured', actionTypeId: 'my-action-type', @@ -2769,6 +3184,17 @@ describe('isPreconfigured()', () => { foo: 'bar', }, }, + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, ], connectorTokenClient: new ConnectorTokenClient({ unsecuredSavedObjectsClient: savedObjectsClientMock.create(), @@ -2782,6 +3208,110 @@ describe('isPreconfigured()', () => { }); }); +describe('isSystemAction()', () => { + test('should return true if the connector is a system connectors', () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + 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: new ConnectorTokenClient({ + unsecuredSavedObjectsClient: savedObjectsClientMock.create(), + encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), + logger, + }), + getEventLogClient, + }); + + expect(actionsClient.isSystemAction('system-connector-.cases')).toEqual(true); + }); + + test('should return false if connector id is not a system action', () => { + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + inMemoryConnectors: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + 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: new ConnectorTokenClient({ + unsecuredSavedObjectsClient: savedObjectsClientMock.create(), + encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), + logger, + }), + getEventLogClient, + }); + + expect(actionsClient.isSystemAction(uuidv4())).toEqual(false); + }); +}); + describe('getGlobalExecutionLogWithAuth()', () => { const opts: GetGlobalExecutionLogParams = { dateStart: '2023-01-09T08:55:56-08:00', diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 7905b7a0da6df5..db25e302a4ac02 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -45,7 +45,7 @@ import { ActionResult, FindActionResult, RawAction, - PreConfiguredAction, + InMemoryConnector, ActionTypeExecutorResult, ConnectorTokenClientContract, } from './types'; @@ -115,7 +115,7 @@ interface ConstructorOptions { scopedClusterClient: IScopedClusterClient; actionTypeRegistry: ActionTypeRegistry; unsecuredSavedObjectsClient: SavedObjectsClientContract; - preconfiguredActions: PreConfiguredAction[]; + inMemoryConnectors: InMemoryConnector[]; actionExecutor: ActionExecutorContract; executionEnqueuer: ExecutionEnqueuer; ephemeralExecutionEnqueuer: ExecutionEnqueuer; @@ -139,7 +139,7 @@ export class ActionsClient { private readonly scopedClusterClient: IScopedClusterClient; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; private readonly actionTypeRegistry: ActionTypeRegistry; - private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly inMemoryConnectors: InMemoryConnector[]; private readonly actionExecutor: ActionExecutorContract; private readonly request: KibanaRequest; private readonly authorization: ActionsAuthorization; @@ -157,7 +157,7 @@ export class ActionsClient { kibanaIndices, scopedClusterClient, unsecuredSavedObjectsClient, - preconfiguredActions, + inMemoryConnectors, actionExecutor, executionEnqueuer, ephemeralExecutionEnqueuer, @@ -174,7 +174,7 @@ export class ActionsClient { this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.scopedClusterClient = scopedClusterClient; this.kibanaIndices = kibanaIndices; - this.preconfiguredActions = preconfiguredActions; + this.inMemoryConnectors = inMemoryConnectors; this.actionExecutor = actionExecutor; this.executionEnqueuer = executionEnqueuer; this.ephemeralExecutionEnqueuer = ephemeralExecutionEnqueuer; @@ -196,17 +196,6 @@ export class ActionsClient { }: CreateOptions): Promise { const id = options?.id || SavedObjectsUtils.generateId(); - if (this.preconfiguredActions.some((preconfiguredAction) => preconfiguredAction.id === id)) { - throw Boom.badRequest( - i18n.translate('xpack.actions.serverSideErrors.predefinedIdConnectorAlreadyExists', { - defaultMessage: 'This {id} already exist in preconfigured action.', - values: { - id, - }, - }) - ); - } - try { await this.authorization.ensureAuthorized('create', actionTypeId); } catch (error) { @@ -220,6 +209,33 @@ export class ActionsClient { throw error; } + const foundInMemoryConnector = this.inMemoryConnectors.find((connector) => connector.id === id); + + if ( + this.actionTypeRegistry.isSystemActionType(actionTypeId) || + foundInMemoryConnector?.isSystemAction + ) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.systemActionCreationForbidden', { + defaultMessage: 'System action creation is forbidden. Action type: {actionTypeId}.', + values: { + actionTypeId, + }, + }) + ); + } + + if (foundInMemoryConnector?.isPreconfigured) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.predefinedIdConnectorAlreadyExists', { + defaultMessage: 'This {id} already exists in a preconfigured action.', + values: { + id, + }, + }) + ); + } + const actionType = this.actionTypeRegistry.get(actionTypeId); const configurationUtilities = this.actionTypeRegistry.getUtils(); const validatedActionTypeConfig = validateConfig(actionType, config, { @@ -272,13 +288,25 @@ export class ActionsClient { try { await this.authorization.ensureAuthorized('update'); - if ( - this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !== - undefined - ) { + const foundInMemoryConnector = this.inMemoryConnectors.find( + (connector) => connector.id === id + ); + + if (foundInMemoryConnector?.isSystemAction) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.systemActionUpdateForbidden', { + defaultMessage: 'System action {id} can not be updated.', + values: { + id, + }, + }) + ); + } + + if (foundInMemoryConnector?.isPreconfigured) { throw new PreconfiguredActionDisabledModificationError( i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', { - defaultMessage: 'Preconfigured action {id} is not allowed to update.', + defaultMessage: 'Preconfigured action {id} can not be updated.', values: { id, }, @@ -380,10 +408,9 @@ export class ActionsClient { throw error; } - const preconfiguredActionsList = this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === id - ); - if (preconfiguredActionsList !== undefined) { + const foundInMemoryConnector = this.inMemoryConnectors.find((connector) => connector.id === id); + + if (foundInMemoryConnector !== undefined) { this.auditLogger?.log( connectorAuditEvent({ action: ConnectorAuditAction.GET, @@ -393,11 +420,11 @@ export class ActionsClient { return { id, - actionTypeId: preconfiguredActionsList.actionTypeId, - name: preconfiguredActionsList.name, - isPreconfigured: true, - isSystemAction: false, - isDeprecated: isConnectorDeprecated(preconfiguredActionsList), + actionTypeId: foundInMemoryConnector.actionTypeId, + name: foundInMemoryConnector.name, + isPreconfigured: foundInMemoryConnector.isPreconfigured, + isSystemAction: foundInMemoryConnector.isSystemAction, + isDeprecated: isConnectorDeprecated(foundInMemoryConnector), }; } @@ -423,7 +450,7 @@ export class ActionsClient { } /** - * Get all actions with preconfigured list + * Get all actions with in-memory connectors */ public async getAll(): Promise { try { @@ -458,20 +485,20 @@ export class ActionsClient { const mergedResult = [ ...savedObjectsActions, - ...this.preconfiguredActions.map((preconfiguredAction) => ({ - id: preconfiguredAction.id, - actionTypeId: preconfiguredAction.actionTypeId, - name: preconfiguredAction.name, - isPreconfigured: true, - isDeprecated: isConnectorDeprecated(preconfiguredAction), - isSystemAction: false, + ...this.inMemoryConnectors.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); } /** - * Get bulk actions with preconfigured list + * Get bulk actions with in-memory list */ public async getBulk(ids: string[]): Promise { try { @@ -490,17 +517,19 @@ export class ActionsClient { } const actionResults = new Array(); + for (const actionId of ids) { - const action = this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === actionId + const action = this.inMemoryConnectors.find( + (inMemoryConnector) => inMemoryConnector.id === actionId ); + if (action !== undefined) { actionResults.push(action); } } // Fetch action objects in bulk - // Excluding preconfigured actions to avoid an not found error, which is already added + // Excluding in-memory actions to avoid an not found error, which is already added const actionSavedObjectsIds = [ ...new Set( ids.filter( @@ -531,6 +560,7 @@ export class ActionsClient { } actionResults.push(actionFromSavedObject(action, isConnectorDeprecated(action.attributes))); } + return actionResults; } @@ -632,10 +662,22 @@ export class ActionsClient { try { await this.authorization.ensureAuthorized('delete'); - if ( - this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !== - undefined - ) { + const foundInMemoryConnector = this.inMemoryConnectors.find( + (connector) => connector.id === id + ); + + if (foundInMemoryConnector?.isSystemAction) { + throw Boom.badRequest( + i18n.translate('xpack.actions.serverSideErrors.systemActionDeletionForbidden', { + defaultMessage: 'System action {id} is not allowed to delete.', + values: { + id, + }, + }) + ); + } + + if (foundInMemoryConnector?.isPreconfigured) { throw new PreconfiguredActionDisabledModificationError( i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', { defaultMessage: 'Preconfigured action {id} is not allowed to delete.', @@ -765,7 +807,15 @@ export class ActionsClient { } public isPreconfigured(connectorId: string): boolean { - return !!this.preconfiguredActions.find((preconfigured) => preconfigured.id === connectorId); + return !!this.inMemoryConnectors.find( + (connector) => connector.isPreconfigured && connector.id === connectorId + ); + } + + public isSystemAction(connectorId: string): boolean { + return !!this.inMemoryConnectors.find( + (connector) => connector.isSystemAction && connector.id === connectorId + ); } public async getGlobalExecutionLogWithAuth({ diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index ec746ee62862bf..75a1ecf333311a 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -32,7 +32,7 @@ describe('execute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -103,7 +103,7 @@ describe('execute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -176,7 +176,7 @@ describe('execute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -247,7 +247,7 @@ describe('execute()', () => { taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), isESOCanEncrypt: true, - preconfiguredActions: [ + inMemoryConnectors: [ { id: '123', actionTypeId: 'mock-action-preconfigured', @@ -322,12 +322,95 @@ describe('execute()', () => { ); }); + test('schedules the action with all given parameters with a system action', async () => { + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + const source = { type: 'alert', id: uuidv4() }; + + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }); + + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + + await executeFn(savedObjectsClient, { + id: 'system-connector-.cases', + params: { baz: false }, + spaceId: 'default', + executionId: 'system-connector-.casesabc', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + source: asSavedObjectExecutionSource(source), + }); + + expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.cases", + }, + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: 'system-connector-.cases', + params: { baz: false }, + executionId: 'system-connector-.casesabc', + source: 'SAVED_OBJECT', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + }, + { + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + ], + } + ); + }); + test('schedules the action with all given parameters with a preconfigured action and relatedSavedObjects', async () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), isESOCanEncrypt: true, - preconfiguredActions: [ + inMemoryConnectors: [ { id: '123', actionTypeId: 'mock-action-preconfigured', @@ -423,12 +506,113 @@ describe('execute()', () => { ); }); + test('schedules the action with all given parameters with a system action and relatedSavedObjects', async () => { + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + const source = { type: 'alert', id: uuidv4() }; + + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + await executeFn(savedObjectsClient, { + id: 'system-connector-.cases', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + source: asSavedObjectExecutionSource(source), + executionId: 'system-connector-.casesabc', + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }); + expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.cases", + }, + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: 'system-connector-.cases', + params: { baz: false }, + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + executionId: 'system-connector-.casesabc', + source: 'SAVED_OBJECT', + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + { + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + { + id: 'some-id', + name: 'related_some-type_0', + type: 'some-type', + }, + ], + } + ); + }); + test('throws when passing isESOCanEncrypt with false as a value', async () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, isESOCanEncrypt: false, actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }); await expect( executeFn(savedObjectsClient, { @@ -449,7 +633,7 @@ describe('execute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -481,7 +665,7 @@ describe('execute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, - preconfiguredActions: [], + inMemoryConnectors: [], }); mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { throw new Error('Fail'); @@ -513,7 +697,7 @@ describe('execute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, - preconfiguredActions: [ + inMemoryConnectors: [ { actionTypeId: 'mock-action', config: {}, @@ -553,6 +737,53 @@ describe('execute()', () => { expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); }); + + test('should skip ensure action type if action type is system action and license is valid', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + isESOCanEncrypt: true, + actionTypeRegistry: mockedActionTypeRegistry, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + + await executeFn(savedObjectsClient, { + id: 'system-connector-.case', + params: { baz: false }, + spaceId: 'default', + executionId: 'system-connector-.caseabc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + }); + + expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); + }); }); describe('bulkExecute()', () => { @@ -562,7 +793,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -650,7 +881,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -741,7 +972,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, actionTypeRegistry, isESOCanEncrypt: true, - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -825,7 +1056,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), isESOCanEncrypt: true, - preconfiguredActions: [ + inMemoryConnectors: [ { id: '123', actionTypeId: 'mock-action-preconfigured', @@ -917,12 +1148,109 @@ describe('bulkExecute()', () => { ); }); + test('schedules the action with all given parameters with a system action', async () => { + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + const source = { type: 'alert', id: uuidv4() }; + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: 'system-connector-.cases', + }, + references: [], + }, + ], + }); + await executeFn(savedObjectsClient, [ + { + id: 'system-connector-.cases', + params: { baz: false }, + spaceId: 'default', + executionId: 'system-connector-.casesabc', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + source: asSavedObjectExecutionSource(source), + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.cases", + }, + ], + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: 'system-connector-.cases', + params: { baz: false }, + executionId: 'system-connector-.casesabc', + source: 'SAVED_OBJECT', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + }, + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + ], + }, + ], + { refresh: false } + ); + }); + test('schedules the action with all given parameters with a preconfigured action and relatedSavedObjects', async () => { const executeFn = createBulkExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), isESOCanEncrypt: true, - preconfiguredActions: [ + inMemoryConnectors: [ { id: '123', actionTypeId: 'mock-action-preconfigured', @@ -1035,12 +1363,130 @@ describe('bulkExecute()', () => { ); }); + test('schedules the action with all given parameters with a system action and relatedSavedObjects', async () => { + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + const source = { type: 'alert', id: uuidv4() }; + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: 'system-connector-.cases', + }, + references: [], + }, + ], + }); + await executeFn(savedObjectsClient, [ + { + id: 'system-connector-.cases', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + source: asSavedObjectExecutionSource(source), + executionId: 'system-connector-.casesabc', + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.cases", + }, + ], + ] + `); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: 'action_task_params', + attributes: { + actionId: 'system-connector-.cases', + params: { baz: false }, + apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'), + executionId: 'system-connector-.casesabc', + source: 'SAVED_OBJECT', + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + { + id: 'some-id', + name: 'related_some-type_0', + type: 'some-type', + }, + ], + }, + ], + { refresh: false } + ); + }); + test('throws when passing isESOCanEncrypt with false as a value', async () => { const executeFn = createBulkExecutionEnqueuerFunction({ taskManager: mockTaskManager, isESOCanEncrypt: false, actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }); await expect( executeFn(savedObjectsClient, [ @@ -1063,7 +1509,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -1101,7 +1547,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, - preconfiguredActions: [], + inMemoryConnectors: [], }); mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { throw new Error('Fail'); @@ -1139,7 +1585,7 @@ describe('bulkExecute()', () => { taskManager: mockTaskManager, isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, - preconfiguredActions: [ + inMemoryConnectors: [ { actionTypeId: 'mock-action', config: {}, @@ -1191,4 +1637,63 @@ describe('bulkExecute()', () => { expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); }); + + test('should skip ensure action type if action type is system action and license is valid', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + isESOCanEncrypt: true, + actionTypeRegistry: mockedActionTypeRegistry, + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '123', + type: 'action', + attributes: { + actionTypeId: '.cases', + }, + references: [], + }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + ], + }); + + await executeFn(savedObjectsClient, [ + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '123abc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + }, + ]); + + expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index c33bcc6923d8a2..6752b17fd5ffde 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -10,7 +10,7 @@ import { RunNowResult, TaskManagerStartContract } from '@kbn/task-manager-plugin import { RawAction, ActionTypeRegistryContract, - PreConfiguredAction, + InMemoryConnector, ActionTaskExecutorParams, } from './types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; @@ -21,7 +21,7 @@ interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; isESOCanEncrypt: boolean; actionTypeRegistry: ActionTypeRegistryContract; - preconfiguredActions: PreConfiguredAction[]; + inMemoryConnectors: InMemoryConnector[]; } export interface ExecuteOptions @@ -39,8 +39,8 @@ interface ActionTaskParams } export interface GetConnectorsResult { - connector: PreConfiguredAction | RawAction; - isPreconfigured: boolean; + connector: InMemoryConnector | RawAction; + isInMemory: boolean; id: string; } @@ -58,7 +58,7 @@ export function createExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, isESOCanEncrypt, - preconfiguredActions, + inMemoryConnectors, }: CreateExecuteFunctionOptions): ExecutionEnqueuer { return async function execute( unsecuredSavedObjectsClient: SavedObjectsClientContract, @@ -79,9 +79,9 @@ export function createExecutionEnqueuerFunction({ ); } - const { action, isPreconfigured } = await getAction( + const { action, isInMemory } = await getAction( unsecuredSavedObjectsClient, - preconfiguredActions, + inMemoryConnectors, id ); validateCanActionBeUsed(action); @@ -94,7 +94,7 @@ export function createExecutionEnqueuerFunction({ // Get saved object references from action ID and relatedSavedObjects const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( id, - isPreconfigured, + isInMemory, relatedSavedObjects ); const executionSourceReference = executionSourceAsSavedObjectReferences(source); @@ -139,7 +139,7 @@ export function createBulkExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, isESOCanEncrypt, - preconfiguredActions, + inMemoryConnectors, }: CreateExecuteFunctionOptions): BulkExecutionEnqueuer { return async function execute( unsecuredSavedObjectsClient: SavedObjectsClientContract, @@ -153,15 +153,16 @@ export function createBulkExecutionEnqueuerFunction({ const actionTypeIds: Record = {}; const spaceIds: Record = {}; - const connectorIsPreconfigured: Record = {}; + const connectorIsInMemory: Record = {}; const connectorIds = [...new Set(actionsToExecute.map((action) => action.id))]; const connectors = await getConnectors( unsecuredSavedObjectsClient, - preconfiguredActions, + inMemoryConnectors, connectorIds ); + connectors.forEach((c) => { - const { id, connector, isPreconfigured } = c; + const { id, connector, isInMemory } = c; validateCanActionBeUsed(connector); const { actionTypeId } = connector; @@ -170,16 +171,17 @@ export function createBulkExecutionEnqueuerFunction({ } actionTypeIds[id] = actionTypeId; - connectorIsPreconfigured[id] = isPreconfigured; + connectorIsInMemory[id] = isInMemory; }); const actions = actionsToExecute.map((actionToExecute) => { // Get saved object references from action ID and relatedSavedObjects const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( actionToExecute.id, - connectorIsPreconfigured[actionToExecute.id], + connectorIsInMemory[actionToExecute.id], actionToExecute.relatedSavedObjects ); + const executionSourceReference = executionSourceAsSavedObjectReferences( actionToExecute.source ); @@ -229,13 +231,13 @@ export function createBulkExecutionEnqueuerFunction({ export function createEphemeralExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, - preconfiguredActions, + inMemoryConnectors, }: CreateExecuteFunctionOptions): ExecutionEnqueuer { return async function execute( unsecuredSavedObjectsClient: SavedObjectsClientContract, { id, params, spaceId, source, consumer, apiKey, executionId }: ExecuteOptions ): Promise { - const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id); + const { action } = await getAction(unsecuredSavedObjectsClient, inMemoryConnectors, id); validateCanActionBeUsed(action); const { actionTypeId } = action; @@ -266,7 +268,7 @@ export function createEphemeralExecutionEnqueuerFunction({ }; } -function validateCanActionBeUsed(action: PreConfiguredAction | RawAction) { +function validateCanActionBeUsed(action: InMemoryConnector | RawAction) { const { name, isMissingSecrets } = action; if (isMissingSecrets) { throw new Error( @@ -290,30 +292,32 @@ function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorO async function getAction( unsecuredSavedObjectsClient: SavedObjectsClientContract, - preconfiguredActions: PreConfiguredAction[], + inMemoryConnectors: InMemoryConnector[], actionId: string -): Promise<{ action: PreConfiguredAction | RawAction; isPreconfigured: boolean }> { - const pcAction = preconfiguredActions.find((action) => action.id === actionId); - if (pcAction) { - return { action: pcAction, isPreconfigured: true }; +): Promise<{ action: InMemoryConnector | RawAction; isInMemory: boolean }> { + const inMemoryAction = inMemoryConnectors.find((action) => action.id === actionId); + + if (inMemoryAction) { + return { action: inMemoryAction, isInMemory: true }; } const { attributes } = await unsecuredSavedObjectsClient.get('action', actionId); - return { action: attributes, isPreconfigured: false }; + return { action: attributes, isInMemory: false }; } async function getConnectors( unsecuredSavedObjectsClient: SavedObjectsClientContract, - preconfiguredConnectors: PreConfiguredAction[], + inMemoryConnectors: InMemoryConnector[], connectorIds: string[] ): Promise { const result: GetConnectorsResult[] = []; const connectorIdsToFetch = []; for (const connectorId of connectorIds) { - const pcConnector = preconfiguredConnectors.find((connector) => connector.id === connectorId); + const pcConnector = inMemoryConnectors.find((connector) => connector.id === connectorId); + if (pcConnector) { - result.push({ connector: pcConnector, isPreconfigured: true, id: connectorId }); + result.push({ connector: pcConnector, isInMemory: true, id: connectorId }); } else { connectorIdsToFetch.push(connectorId); } @@ -330,7 +334,7 @@ async function getConnectors( for (const item of bulkGetResult.saved_objects) { if (item.error) throw item.error; result.push({ - isPreconfigured: false, + isInMemory: false, connector: item.attributes, id: item.id, }); diff --git a/x-pack/plugins/actions/server/create_system_actions.test.ts b/x-pack/plugins/actions/server/create_system_actions.test.ts new file mode 100644 index 00000000000000..55b7d43bf5631f --- /dev/null +++ b/x-pack/plugins/actions/server/create_system_actions.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { createSystemConnectors } from './create_system_actions'; + +const actionTypes = [ + { + id: 'action-type', + name: 'My action type', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic' as const, + supportedFeatureIds: ['alerting'], + isSystemActionType: false, + }, + { + id: 'system-action-type-2', + name: 'My system action type', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic' as const, + supportedFeatureIds: ['alerting'], + isSystemActionType: true, + }, +]; + +describe('createSystemConnectors', () => { + it('creates the system actions correctly', () => { + expect(createSystemConnectors(actionTypes)).toEqual([ + { + id: 'system-connector-system-action-type-2', + actionTypeId: 'system-action-type-2', + name: 'System action: system-action-type-2', + secrets: {}, + config: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ]); + }); +}); diff --git a/x-pack/plugins/actions/server/create_system_actions.ts b/x-pack/plugins/actions/server/create_system_actions.ts new file mode 100644 index 00000000000000..f6079ea9402223 --- /dev/null +++ b/x-pack/plugins/actions/server/create_system_actions.ts @@ -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 { ActionType } from '../common'; +import { InMemoryConnector } from './types'; + +export const createSystemConnectors = (actionTypes: ActionType[]): InMemoryConnector[] => { + const systemActionTypes = actionTypes.filter((actionType) => actionType.isSystemActionType); + + const systemConnectors: InMemoryConnector[] = systemActionTypes.map((systemActionType) => ({ + id: `system-connector-${systemActionType.id}`, + actionTypeId: systemActionType.id, + name: `System action: ${systemActionType.id}`, + isMissingSecrets: false, + config: {}, + secrets: {}, + isDeprecated: false, + isPreconfigured: false, + isSystemAction: true, + })); + + return systemConnectors; +}; diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts index 8e75a889c3410d..2bbfab40b7318d 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts @@ -21,58 +21,63 @@ const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); beforeEach(() => jest.resetAllMocks()); describe('bulkExecute()', () => { - test('schedules the actions with all given parameters with a preconfigured connector', async () => { - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); + test.each([ + [true, false], + [false, true], + ])( + 'schedules the actions with all given parameters with an in-memory connector: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + inMemoryConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, + }, + ], + }); - internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ - { - id: '234', - type: 'action_task_params', - attributes: { - actionId: '123', + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], }, - references: [], + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), }, { - id: '345', - type: 'action_task_params', - attributes: { - actionId: '123', - }, - references: [], + id: '123', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), }, - ], - }); - await executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - { - id: '123', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - ]); - expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -101,90 +106,96 @@ describe('bulkExecute()', () => { ] `); - expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - apiKey: null, - source: 'NOTIFICATION', - }, - references: [], - }, - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: true }, - apiKey: null, - source: 'NOTIFICATION', - }, - references: [], - }, - ]); - }); - - test('schedules the actions with all given parameters with a preconfigured connector and source specified', async () => { - const sourceUuid = uuidv4(); - const source = { type: 'alert', id: sourceUuid }; - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - - internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ { - id: '234', type: 'action_task_params', attributes: { actionId: '123', + params: { baz: false }, + apiKey: null, + source: 'NOTIFICATION', }, - references: [ - { - id: sourceUuid, - name: 'source', - type: 'alert', - }, - ], + references: [], }, { - id: '345', type: 'action_task_params', attributes: { actionId: '123', + params: { baz: true }, + apiKey: null, + source: 'NOTIFICATION', }, references: [], }, - ], - }); - await executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asSavedObjectExecutionSource(source), - }, - { - id: '123', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - ]); - expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + ]); + } + ); + + test.each([ + [true, false], + [false, true], + ])( + 'schedules the actions with all given parameters with an in-memory connector and source specified: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const sourceUuid = uuidv4(); + const source = { type: 'alert', id: sourceUuid }; + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + inMemoryConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, + }, + ], + }); + + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asSavedObjectExecutionSource(source), + }, + { + id: '123', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -213,63 +224,14 @@ describe('bulkExecute()', () => { ] `); - expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - apiKey: null, - source: 'SAVED_OBJECT', - }, - references: [ - { - id: sourceUuid, - name: 'source', - type: 'alert', - }, - ], - }, - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: true }, - apiKey: null, - source: 'NOTIFICATION', - }, - references: [], - }, - ]); - }); - - test('schedules the actions with all given parameters with a preconfigured connector and relatedSavedObjects specified', async () => { - const sourceUuid = uuidv4(); - const source = { type: 'alert', id: sourceUuid }; - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - - internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ - saved_objects: [ + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ { - id: '234', type: 'action_task_params', attributes: { actionId: '123', + params: { baz: false }, + apiKey: null, + source: 'SAVED_OBJECT', }, references: [ { @@ -280,42 +242,97 @@ describe('bulkExecute()', () => { ], }, { - id: '345', type: 'action_task_params', attributes: { actionId: '123', + params: { baz: true }, + apiKey: null, + source: 'NOTIFICATION', }, - references: [ + references: [], + }, + ]); + } + ); + + test.each([ + [true, false], + [false, true], + ])( + 'schedules the actions with all given parameters with an in-memory connector and relatedSavedObjects specified: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const sourceUuid = uuidv4(); + const source = { type: 'alert', id: sourceUuid }; + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + inMemoryConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, + }, + ], + }); + + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: 'some-id', + name: 'related_some-type_0', + type: 'some-type', + }, + ], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asSavedObjectExecutionSource(source), + }, + { + id: '123', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + relatedSavedObjects: [ { id: 'some-id', - name: 'related_some-type_0', + namespace: 'some-namespace', type: 'some-type', }, ], }, - ], - }); - await executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asSavedObjectExecutionSource(source), - }, - { - id: '123', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - relatedSavedObjects: [ - { - id: 'some-id', - namespace: 'some-namespace', - type: 'some-type', - }, - ], - }, - ]); - expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); - expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -344,164 +361,183 @@ describe('bulkExecute()', () => { ] `); - expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: false }, - apiKey: null, - source: 'SAVED_OBJECT', + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + apiKey: null, + source: 'SAVED_OBJECT', + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], }, - references: [ - { - id: sourceUuid, - name: 'source', - type: 'alert', + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: true }, + apiKey: null, + source: 'NOTIFICATION', + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + }, + ], }, - ], - }, - { - type: 'action_task_params', - attributes: { - actionId: '123', - params: { baz: true }, - apiKey: null, - source: 'NOTIFICATION', - relatedSavedObjects: [ + references: [ { - id: 'related_some-type_0', - namespace: 'some-namespace', + id: 'some-id', + name: 'related_some-type_0', type: 'some-type', }, ], }, - references: [ + ]); + } + ); + + test.each([ + [true, false], + [false, true], + ])( + 'throws when scheduling action using non in-memory connector: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + inMemoryConnectors: [ { - id: 'some-id', - name: 'related_some-type_0', - type: 'some-type', + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, }, ], - }, - ]); - }); - - test('throws when scheduling action using non preconfigured connector', async () => { - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - await expect( - executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - { - id: 'not-preconfigured', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - ]) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"not-preconfigured are not preconfigured connectors and can't be scheduled for unsecured actions execution"` - ); - }); + }); + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + { + id: 'not-preconfigured', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"not-preconfigured are not in-memory connectors and can't be scheduled for unsecured actions execution"` + ); + } + ); - test('throws when connector type is not enabled', async () => { - const mockedConnectorTypeRegistry = actionTypeRegistryMock.create(); - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: mockedConnectorTypeRegistry, - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - mockedConnectorTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { - throw new Error('Fail'); - }); + test.each([ + [true, false], + [false, true], + ])( + 'throws when connector type is not enabled: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const mockedConnectorTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: mockedConnectorTypeRegistry, + inMemoryConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, + }, + ], + }); + mockedConnectorTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); - await expect( - executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - { - id: '123', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - ]) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - }); + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + { + id: '123', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + } + ); - test('throws when scheduling action using non allow-listed preconfigured connector', async () => { - const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ - taskManager: mockTaskManager, - connectorTypeRegistry: actionTypeRegistryMock.create(), - preconfiguredConnectors: [ - { - id: '123', - actionTypeId: '.email', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - { - id: '456', - actionTypeId: 'not-in-allowlist', - config: {}, - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, - name: 'x', - secrets: {}, - }, - ], - }); - await expect( - executeFn(internalSavedObjectsRepository, [ - { - id: '123', - params: { baz: false }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - { - id: '456', - params: { baz: true }, - source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), - }, - ]) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"not-in-allowlist actions cannot be scheduled for unsecured actions execution"` - ); - }); + test.each([ + [true, false], + [false, true], + ])( + 'throws when scheduling action using non allow-listed in-memory connector: isPreconfigured: %s, isSystemAction: %s', + async (isPreconfigured, isSystemAction) => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + inMemoryConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured, + isDeprecated: false, + isSystemAction, + name: 'x', + secrets: {}, + }, + { + id: '456', + actionTypeId: 'not-in-allowlist', + config: {}, + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + name: 'x', + secrets: {}, + }, + ], + }); + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + { + id: '456', + params: { baz: true }, + source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }), + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"not-in-allowlist actions cannot be scheduled for unsecured actions execution"` + ); + } + ); }); diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index b1ad90d9093eb0..585f442c68e2f0 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -9,7 +9,7 @@ import { ISavedObjectsRepository, SavedObjectsBulkResponse } from '@kbn/core/ser import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { ActionTypeRegistryContract as ConnectorTypeRegistryContract, - PreConfiguredAction as PreconfiguredConnector, + InMemoryConnector, } from './types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; @@ -21,7 +21,7 @@ const ALLOWED_CONNECTOR_TYPE_IDS = ['.email']; interface CreateBulkUnsecuredExecuteFunctionOptions { taskManager: TaskManagerStartContract; connectorTypeRegistry: ConnectorTypeRegistryContract; - preconfiguredConnectors: PreconfiguredConnector[]; + inMemoryConnectors: InMemoryConnector[]; } export interface ExecuteOptions @@ -42,7 +42,7 @@ export type BulkUnsecuredExecutionEnqueuer = ( export function createBulkUnsecuredExecutionEnqueuerFunction({ taskManager, connectorTypeRegistry, - preconfiguredConnectors, + inMemoryConnectors, }: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer { return async function execute( internalSavedObjectsRepository: ISavedObjectsRepository, @@ -51,24 +51,23 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ const connectorTypeIds: Record = {}; const connectorIds = [...new Set(actionsToExecute.map((action) => action.id))]; - const notPreconfiguredConnectors = connectorIds.filter( - (connectorId) => - preconfiguredConnectors.find((connector) => connector.id === connectorId) == null + const notInMemoryConnectors = connectorIds.filter( + (connectorId) => inMemoryConnectors.find((connector) => connector.id === connectorId) == null ); - if (notPreconfiguredConnectors.length > 0) { + if (notInMemoryConnectors.length > 0) { throw new Error( - `${notPreconfiguredConnectors.join( + `${notInMemoryConnectors.join( ',' - )} are not preconfigured connectors and can't be scheduled for unsecured actions execution` + )} are not in-memory connectors and can't be scheduled for unsecured actions execution` ); } - const connectors: PreconfiguredConnector[] = connectorIds + const connectors: InMemoryConnector[] = connectorIds .map((connectorId) => - preconfiguredConnectors.find((pConnector) => pConnector.id === connectorId) + inMemoryConnectors.find((inMemoryConnector) => inMemoryConnector.id === connectorId) ) - .filter(Boolean) as PreconfiguredConnector[]; + .filter(Boolean) as InMemoryConnector[]; connectors.forEach((connector) => { const { id, actionTypeId } = connector; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 15f3c2fa7e20a3..3deddfc9c16c6a 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -22,7 +22,7 @@ export type { ActionResult, ActionTypeExecutorOptions, ActionType, - PreConfiguredAction, + InMemoryConnector, ActionsApiRequestHandlerContext, FindActionResult, } from './types'; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 9b2e2f96390c79..af39f914fc720c 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -51,7 +51,7 @@ actionExecutor.initialize({ actionTypeRegistry, encryptedSavedObjectsClient, eventLogger, - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'preconfigured', name: 'Preconfigured', @@ -66,6 +66,16 @@ actionExecutor.initialize({ isDeprecated: false, isSystemAction: false, }, + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, ], }); @@ -662,6 +672,133 @@ test('successfully executes with preconfigured connector', async () => { `); }); +test('successfully executes with system connector', async () => { + const actionType: jest.Mocked = { + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.any() }, + secrets: { schema: schema.any() }, + params: { schema: schema.any() }, + }, + executor: jest.fn(), + }; + + actionTypeRegistry.get.mockReturnValueOnce(actionType); + await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' }); + + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).not.toHaveBeenCalled(); + + expect(actionTypeRegistry.get).toHaveBeenCalledWith('.cases'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith( + 'system-connector-.cases', + '.cases', + { + notifyUsage: true, + } + ); + + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: 'system-connector-.cases', + services: expect.anything(), + config: {}, + secrets: {}, + params: { foo: true }, + logger: loggerMock, + }); + + expect(loggerMock.debug).toBeCalledWith( + 'executing action .cases:system-connector-.cases: System action: .cases' + ); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + "kind": "action", + }, + "kibana": Object { + "action": Object { + "execution": Object { + "uuid": "2", + }, + "id": "system-connector-.cases", + "name": "System action: .cases", + }, + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "system-connector-.cases", + "namespace": "some-namespace", + "rel": "primary", + "space_agnostic": true, + "type": "action", + "type_id": ".cases", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action started: .cases:system-connector-.cases: System action: .cases", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "kind": "action", + "outcome": "success", + }, + "kibana": Object { + "action": Object { + "execution": Object { + "uuid": "2", + }, + "id": "system-connector-.cases", + "name": "System action: .cases", + }, + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "system-connector-.cases", + "namespace": "some-namespace", + "rel": "primary", + "space_agnostic": true, + "type": "action", + "type_id": ".cases", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action executed: .cases:system-connector-.cases: System action: .cases", + "user": Object { + "id": "123", + "name": "coolguy", + }, + }, + ], + ] + `); +}); + test('successfully executes as a task', async () => { const actionType: jest.Mocked = { id: 'test', @@ -949,6 +1086,51 @@ test('should not throws an error if actionType is preconfigured', async () => { }); }); +test('should not throws an error if actionType is system action', async () => { + const actionType: jest.Mocked = { + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.any() }, + secrets: { schema: schema.any() }, + params: { schema: schema.any() }, + }, + executor: jest.fn(), + }; + + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + name: '1', + actionTypeId: '.cases', + config: {}, + secrets: {}, + }, + references: [], + }; + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { + throw new Error('not enabled for test'); + }); + actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true); + await actionExecutor.execute(executeParams); + + expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0); + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: '1', + services: expect.anything(), + config: {}, + secrets: {}, + params: { foo: true }, + logger: loggerMock, + }); +}); + test('throws an error when passing isESOCanEncrypt with value of false', async () => { const customActionExecutor = new ActionExecutor({ isESOCanEncrypt: false }); customActionExecutor.initialize({ @@ -958,7 +1140,7 @@ test('throws an error when passing isESOCanEncrypt with value of false', async ( actionTypeRegistry, encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), - preconfiguredActions: [], + inMemoryConnectors: [], }); await expect( customActionExecutor.execute(executeParams) @@ -976,7 +1158,7 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f actionTypeRegistry, encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), - preconfiguredActions: [ + inMemoryConnectors: [ { id: 'preconfigured', name: 'Preconfigured', @@ -1117,6 +1299,155 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f `); }); +test('should not throw error if action is system action and isESOCanEncrypt is false', async () => { + const customActionExecutor = new ActionExecutor({ isESOCanEncrypt: false }); + customActionExecutor.initialize({ + logger: loggingSystemMock.create().get(), + spaces: spacesMock, + getServices: () => services, + actionTypeRegistry, + encryptedSavedObjectsClient, + eventLogger: eventLoggerMock.create(), + inMemoryConnectors: [ + { + actionTypeId: '.cases', + config: {}, + id: 'system-connector-.cases', + name: 'System action: .cases', + secrets: {}, + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ], + }); + + const actionType: jest.Mocked = { + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.any() }, + secrets: { schema: schema.any() }, + params: { schema: schema.any() }, + }, + executor: jest.fn(), + }; + + actionTypeRegistry.get.mockReturnValueOnce(actionType); + await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' }); + + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).not.toHaveBeenCalled(); + + expect(actionTypeRegistry.get).toHaveBeenCalledWith('.cases'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith( + 'system-connector-.cases', + '.cases', + { + notifyUsage: true, + } + ); + + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: 'system-connector-.cases', + services: expect.anything(), + config: {}, + secrets: {}, + params: { foo: true }, + logger: loggerMock, + }); + + expect(loggerMock.debug).toBeCalledWith( + 'executing action .cases:system-connector-.cases: System action: .cases' + ); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + "kind": "action", + }, + "kibana": Object { + "action": Object { + "execution": Object { + "uuid": "2", + }, + "id": "system-connector-.cases", + "name": "System action: .cases", + }, + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "system-connector-.cases", + "namespace": "some-namespace", + "rel": "primary", + "space_agnostic": true, + "type": "action", + "type_id": ".cases", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action started: .cases:system-connector-.cases: System action: .cases", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "kind": "action", + "outcome": "success", + }, + "kibana": Object { + "action": Object { + "execution": Object { + "uuid": "2", + }, + "id": "system-connector-.cases", + "name": "System action: .cases", + }, + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "system-connector-.cases", + "namespace": "some-namespace", + "rel": "primary", + "space_agnostic": true, + "type": "action", + "type_id": ".cases", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action executed: .cases:system-connector-.cases: System action: .cases", + "user": Object { + "id": "123", + "name": "coolguy", + }, + }, + ], + ] + `); +}); + test('does not log warning when alert executor succeeds', async () => { const executorMock = setupActionExecutorMock(); executorMock.mockResolvedValue({ diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 272d61943ac2ba..4fe18f6ecd5e55 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -25,7 +25,7 @@ import { ActionTypeExecutorRawResult, ActionTypeRegistryContract, GetServicesFunction, - PreConfiguredAction, + InMemoryConnector, RawAction, ValidatorServices, } from '../types'; @@ -46,7 +46,7 @@ export interface ActionExecutorContext { encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; - preconfiguredActions: PreConfiguredAction[]; + inMemoryConnectors: InMemoryConnector[]; } export interface TaskInfo { @@ -118,7 +118,7 @@ export class ActionExecutor { encryptedSavedObjectsClient, actionTypeRegistry, eventLogger, - preconfiguredActions, + inMemoryConnectors, security, } = this.actionExecutorContext!; @@ -129,7 +129,7 @@ export class ActionExecutor { const actionInfo = await getActionInfoInternal( this.isESOCanEncrypt, encryptedSavedObjectsClient, - preconfiguredActions, + inMemoryConnectors, actionId, namespace.namespace ); @@ -186,7 +186,7 @@ export class ActionExecutor { relatedSavedObjects, name, actionExecutionId, - isPreconfigured: this.actionInfo.isPreconfigured, + isInMemory: this.actionInfo.isInMemory, ...(source ? { source } : {}), }); @@ -341,7 +341,7 @@ export class ActionExecutor { source?: ActionExecutionSource; consumer?: string; }) { - const { spaces, encryptedSavedObjectsClient, preconfiguredActions, eventLogger } = + const { spaces, encryptedSavedObjectsClient, inMemoryConnectors, eventLogger } = this.actionExecutorContext!; const spaceId = spaces && spaces.getSpaceId(request); @@ -350,7 +350,7 @@ export class ActionExecutor { this.actionInfo = await getActionInfoInternal( this.isESOCanEncrypt, encryptedSavedObjectsClient, - preconfiguredActions, + inMemoryConnectors, actionId, namespace.namespace ); @@ -385,7 +385,7 @@ export class ActionExecutor { ], relatedSavedObjects, actionExecutionId, - isPreconfigured: this.actionInfo.isPreconfigured, + isInMemory: this.actionInfo.isInMemory, ...(source ? { source } : {}), }); @@ -399,28 +399,29 @@ interface ActionInfo { config: unknown; secrets: unknown; actionId: string; - isPreconfigured?: boolean; + isInMemory?: boolean; } async function getActionInfoInternal( isESOCanEncrypt: boolean, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, - preconfiguredActions: PreConfiguredAction[], + inMemoryConnectors: InMemoryConnector[], actionId: string, namespace: string | undefined ): Promise { - // check to see if it's a pre-configured action first - const pcAction = preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === actionId + // check to see if it's in memory action first + const inMemoryAction = inMemoryConnectors.find( + (inMemoryConnector) => inMemoryConnector.id === actionId ); - if (pcAction) { + + if (inMemoryAction) { return { - actionTypeId: pcAction.actionTypeId, - name: pcAction.name, - config: pcAction.config, - secrets: pcAction.secrets, + actionTypeId: inMemoryAction.actionTypeId, + name: inMemoryAction.name, + config: inMemoryAction.config, + secrets: inMemoryAction.secrets, actionId, - isPreconfigured: true, + isInMemory: true, }; } diff --git a/x-pack/plugins/actions/server/lib/action_task_params_utils.test.ts b/x-pack/plugins/actions/server/lib/action_task_params_utils.test.ts index 98a425ff6fd391..ab2d4305b36d84 100644 --- a/x-pack/plugins/actions/server/lib/action_task_params_utils.test.ts +++ b/x-pack/plugins/actions/server/lib/action_task_params_utils.test.ts @@ -87,13 +87,13 @@ describe('extractSavedObjectReferences()', () => { }); }); - test('correctly skips extracting action id if action is preconfigured', () => { + test('correctly skips extracting action id if action is in-memory', () => { expect(extractSavedObjectReferences('my-action-id', true)).toEqual({ references: [], }); }); - test('correctly extracts related saved object into references array if isPreconfigured is true', () => { + test('correctly extracts related saved object into references array if isInMemory is true', () => { const relatedSavedObjects = [ { id: 'abc', diff --git a/x-pack/plugins/actions/server/lib/action_task_params_utils.ts b/x-pack/plugins/actions/server/lib/action_task_params_utils.ts index 425e8dfb107f2f..98288ef4d06c2e 100644 --- a/x-pack/plugins/actions/server/lib/action_task_params_utils.ts +++ b/x-pack/plugins/actions/server/lib/action_task_params_utils.ts @@ -12,7 +12,7 @@ export const ACTION_REF_NAME = `actionRef`; export function extractSavedObjectReferences( actionId: string, - isPreconfigured: boolean, + isInMemory: boolean, relatedSavedObjects?: RelatedSavedObjects ): { references: SavedObjectReference[]; @@ -21,8 +21,8 @@ export function extractSavedObjectReferences( const references: SavedObjectReference[] = []; const relatedSavedObjectWithRefs: RelatedSavedObjects = []; - // Add action saved object to reference if it is not preconfigured - if (!isPreconfigured) { + // Add action saved object to reference if it is not in-memory action + if (!isInMemory) { references.push({ id: actionId, name: ACTION_REF_NAME, diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts index 28c3a96e145075..cb6390a4b33352 100644 --- a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts @@ -413,7 +413,7 @@ describe('createActionEventLogRecordObject', () => { }); }); - test('created action event "execute" for preconfigured connector with space_agnostic true', async () => { + test('created action event "execute" for in-memory connector with space_agnostic true', async () => { expect( createActionEventLogRecordObject({ actionId: '1', @@ -432,7 +432,7 @@ describe('createActionEventLogRecordObject', () => { }, ], actionExecutionId: '123abc', - isPreconfigured: true, + isInMemory: true, }) ).toStrictEqual({ event: { diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts index 46dcddd9b55c57..4f8bf08966c59d 100644 --- a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts @@ -35,7 +35,7 @@ interface CreateActionEventLogRecordParams { relation?: string; }>; relatedSavedObjects?: RelatedSavedObjects; - isPreconfigured?: boolean; + isInMemory?: boolean; source?: ActionExecutionSource; } @@ -51,7 +51,7 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec relatedSavedObjects, name, actionExecutionId, - isPreconfigured, + isInMemory, actionId, source, } = params; @@ -80,8 +80,8 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec type: so.type, id: so.id, type_id: so.typeId, - // set space_agnostic to true for preconfigured connectors - ...(so.type === 'action' && isPreconfigured ? { space_agnostic: isPreconfigured } : {}), + // set space_agnostic to true for in-memory connectors + ...(so.type === 'action' && isInMemory ? { space_agnostic: isInMemory } : {}), ...(namespace ? { namespace } : {}), })), ...(spaceId ? { space_ids: [spaceId] } : {}), diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts index 06e5380c04f97c..5cd2fec223ffc4 100644 --- a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.test.ts @@ -32,9 +32,6 @@ describe('ensureSufficientLicense()', () => { }); it('allows licenses below gold for allowed connectors', () => { - expect(() => - ensureSufficientLicense({ ...sampleActionType, id: '.case', minimumLicenseRequired: 'basic' }) - ).not.toThrow(); expect(() => ensureSufficientLicense({ ...sampleActionType, diff --git a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts index e7d781a7218bd5..5d1988cdd8fa01 100644 --- a/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts +++ b/x-pack/plugins/actions/server/lib/ensure_sufficient_license.ts @@ -9,14 +9,10 @@ import { LICENSE_TYPE } from '@kbn/licensing-plugin/common/types'; import { ActionType } from '../types'; import { ActionTypeConfig, ActionTypeSecrets, ActionTypeParams } from '../types'; -const CASE_ACTION_TYPE_ID = '.case'; const ServerLogActionTypeId = '.server-log'; const IndexActionTypeId = '.index'; -const ACTIONS_SCOPED_WITHIN_STACK = new Set([ - ServerLogActionTypeId, - IndexActionTypeId, - CASE_ACTION_TYPE_ID, -]); + +const ACTIONS_SCOPED_WITHIN_STACK = new Set([ServerLogActionTypeId, IndexActionTypeId]); export function ensureSufficientLicense< Config extends ActionTypeConfig, diff --git a/x-pack/plugins/actions/server/lib/is_connector_deprecated.ts b/x-pack/plugins/actions/server/lib/is_connector_deprecated.ts index db5138cddc53f6..4ebdeec7728eb8 100644 --- a/x-pack/plugins/actions/server/lib/is_connector_deprecated.ts +++ b/x-pack/plugins/actions/server/lib/is_connector_deprecated.ts @@ -6,10 +6,10 @@ */ import { isPlainObject } from 'lodash'; -import { PreConfiguredAction, RawAction } from '../types'; +import { InMemoryConnector, RawAction } from '../types'; -export type ConnectorWithOptionalDeprecation = Omit & - Pick, 'isDeprecated'>; +export type ConnectorWithOptionalDeprecation = Omit & + Pick, 'isDeprecated'>; const isObject = (obj: unknown): obj is Record => isPlainObject(obj); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index eae6cd5bc06c2b..fb764b2b6f74cd 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -86,7 +86,7 @@ const actionExecutorInitializerParams = { getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()), encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, eventLogger, - preconfiguredActions: [], + inMemoryConnectors: [], }; const taskRunnerFactoryInitializerParams = { spaceIdToNamespace, diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index d14c082f969056..14ae64391177f0 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -45,7 +45,7 @@ const createStartMock = () => { getActionsAuthorizationWithRequest: jest .fn() .mockReturnValue(actionsAuthorizationMock.create()), - preconfiguredActions: [], + inMemoryConnectors: [], renderActionParameterTemplates: jest.fn(), }; return mock; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 0136410920c663..458ff70955c6d5 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -29,6 +29,31 @@ const executor: ExecutorType<{}, {}, {}, void> = async (options) => { return { status: 'ok', actionId: options.actionId }; }; +function getConfig(overrides = {}) { + return { + enabled: true, + enabledActionTypes: ['*'], + allowedHosts: ['*'], + preconfiguredAlertHistoryEsIndex: false, + preconfigured: { + preconfiguredServerLog: { + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + }, + }, + proxyRejectUnauthorizedCertificates: true, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, + rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration('60s'), + enableFooterInEmail: true, + ...overrides, + }; +} + describe('Actions Plugin', () => { describe('setup()', () => { let context: PluginInitializerContext; @@ -136,6 +161,106 @@ describe('Actions Plugin', () => { `"Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); + + it('the actions client should have the correct in-memory connectors', async () => { + context = coreMock.createPluginInitializerContext(getConfig()); + const pluginWithPreconfiguredConnectors = new ActionsPlugin(context); + + const coreStart = coreMock.createStart(); + const pluginsStart = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + eventLog: eventLogMock.createStart(), + }; + + /** + * 1. In the setup of the actions plugin + * the preconfigured connectors are being + * set up. Also, the action router handler context + * is registered + */ + const pluginSetup = await pluginWithPreconfiguredConnectors.setup(coreSetup, { + ...pluginsSetup, + encryptedSavedObjects: { + ...pluginsSetup.encryptedSavedObjects, + canEncrypt: true, + }, + }); + + /** + * 2. We simulate the registration of + * a system action by another plugin + * in the setup + */ + pluginSetup.registerType({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + isSystemActionType: true, + executor, + }); + + const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; + + /** + * 3. On start the system actions are being + * created based on the system action types + * that got registered on step 2 + */ + await pluginWithPreconfiguredConnectors.start(coreStart, pluginsStart); + + const actionsContextHandler = (await handler[1]( + { + core: { + savedObjects: { + client: {}, + }, + elasticsearch: { + client: jest.fn(), + }, + }, + } as unknown as RequestHandlerContext, + httpServerMock.createKibanaRequest(), + httpServerMock.createResponseFactory() + )) as unknown as ActionsApiRequestHandlerContext; + + /** + * 4. We verify that the actions client inside + * the router context has the correct system connectors + * that got set up on start (step 3). + */ + // @ts-expect-error: inMemoryConnectors can be accessed + expect(actionsContextHandler.getActionsClient().inMemoryConnectors).toEqual([ + { + id: 'preconfiguredServerLog', + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + isDeprecated: false, + isPreconfigured: true, + isSystemAction: false, + }, + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isPreconfigured: false, + isSystemAction: true, + isMissingSecrets: false, + }, + ]); + }); }); describe('registerType()', () => { @@ -199,31 +324,6 @@ describe('Actions Plugin', () => { }); describe('isPreconfiguredConnector', () => { - function getConfig(overrides = {}) { - return { - enabled: true, - enabledActionTypes: ['*'], - allowedHosts: ['*'], - preconfiguredAlertHistoryEsIndex: false, - preconfigured: { - preconfiguredServerLog: { - actionTypeId: '.server-log', - name: 'preconfigured-server-log', - config: {}, - secrets: {}, - }, - }, - proxyRejectUnauthorizedCertificates: true, - proxyBypassHosts: undefined, - proxyOnlyHosts: undefined, - rejectUnauthorized: true, - maxResponseContentLength: new ByteSizeValue(1000000), - responseTimeout: moment.duration('60s'), - enableFooterInEmail: true, - ...overrides, - }; - } - function setup(config: ActionsConfig) { context = coreMock.createPluginInitializerContext(config); plugin = new ActionsPlugin(context); @@ -323,32 +423,7 @@ describe('Actions Plugin', () => { }); }); - describe('Preconfigured connectors', () => { - function getConfig(overrides = {}) { - return { - enabled: true, - enabledActionTypes: ['*'], - allowedHosts: ['*'], - preconfiguredAlertHistoryEsIndex: false, - preconfigured: { - preconfiguredServerLog: { - actionTypeId: '.server-log', - name: 'preconfigured-server-log', - config: {}, - secrets: {}, - }, - }, - proxyRejectUnauthorizedCertificates: true, - proxyBypassHosts: undefined, - proxyOnlyHosts: undefined, - rejectUnauthorized: true, - maxResponseContentLength: new ByteSizeValue(1000000), - responseTimeout: moment.duration('60s'), - enableFooterInEmail: true, - ...overrides, - }; - } - + describe('inMemoryConnectors', () => { function setup(config: ActionsConfig) { context = coreMock.createPluginInitializerContext(config); plugin = new ActionsPlugin(context); @@ -370,78 +445,134 @@ describe('Actions Plugin', () => { }; } - it('should handle preconfigured actions', async () => { - setup(getConfig()); - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); - pluginSetup.registerType({ - id: '.server-log', - name: 'Server log', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, + describe('Preconfigured connectors', () => { + it('should handle preconfigured actions', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType({ + id: '.server-log', + name: 'Server log', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + const pluginStart = await plugin.start(coreStart, pluginsStart); + + expect(pluginStart.inMemoryConnectors.length).toEqual(1); + expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe( + true + ); }); - const pluginStart = await plugin.start(coreStart, pluginsStart); + it('should handle preconfiguredAlertHistoryEsIndex = true', async () => { + setup(getConfig({ preconfiguredAlertHistoryEsIndex: true })); - expect(pluginStart.preconfiguredActions.length).toEqual(1); - expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType({ + id: '.index', + name: 'ES Index', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); - it('should handle preconfiguredAlertHistoryEsIndex = true', async () => { - setup(getConfig({ preconfiguredAlertHistoryEsIndex: true })); + const pluginStart = await plugin.start(coreStart, pluginsStart); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); - pluginSetup.registerType({ - id: '.index', - name: 'ES Index', - minimumLicenseRequired: 'basic', - supportedFeatureIds: ['alerting'], - validate: { - config: { schema: schema.object({}) }, - secrets: { schema: schema.object({}) }, - params: { schema: schema.object({}) }, - }, - executor, + expect(pluginStart.inMemoryConnectors.length).toEqual(2); + expect( + pluginStart.isActionExecutable('preconfigured-alert-history-es-index', '.index') + ).toBe(true); }); - const pluginStart = await plugin.start(coreStart, pluginsStart); - - expect(pluginStart.preconfiguredActions.length).toEqual(2); - expect( - pluginStart.isActionExecutable('preconfigured-alert-history-es-index', '.index') - ).toBe(true); + it('should not allow preconfigured connector with same ID as AlertHistoryEsIndexConnectorId', async () => { + setup( + getConfig({ + preconfigured: { + [AlertHistoryEsIndexConnectorId]: { + actionTypeId: '.index', + name: 'clashing preconfigured index connector', + config: {}, + secrets: {}, + }, + }, + }) + ); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); + const pluginStart = await plugin.start(coreStart, pluginsStart); + + expect(pluginStart.inMemoryConnectors.length).toEqual(0); + expect(context.logger.get().warn).toHaveBeenCalledWith( + `Preconfigured connectors cannot have the id "${AlertHistoryEsIndexConnectorId}" because this is a reserved id.` + ); + }); }); - it('should not allow preconfigured connector with same ID as AlertHistoryEsIndexConnectorId', async () => { - setup( - getConfig({ - preconfigured: { - [AlertHistoryEsIndexConnectorId]: { - actionTypeId: '.index', - name: 'clashing preconfigured index connector', - config: {}, - secrets: {}, - }, + describe('System actions', () => { + it('should handle system actions', async () => { + setup(getConfig()); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + + pluginSetup.registerType({ + id: '.cases', + name: 'Cases', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, }, - }) - ); - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = await plugin.start(coreStart, pluginsStart); + isSystemActionType: true, + executor, + }); - expect(pluginStart.preconfiguredActions.length).toEqual(0); - expect(context.logger.get().warn).toHaveBeenCalledWith( - `Preconfigured connectors cannot have the id "${AlertHistoryEsIndexConnectorId}" because this is a reserved id.` - ); + const pluginStart = await plugin.start(coreStart, pluginsStart); + + // inMemoryConnectors holds both preconfigure and system connectors + expect(pluginStart.inMemoryConnectors.length).toEqual(2); + expect(pluginStart.inMemoryConnectors).toEqual([ + { + id: 'preconfiguredServerLog', + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + isDeprecated: false, + isPreconfigured: true, + isSystemAction: false, + }, + { + id: 'system-connector-.cases', + actionTypeId: '.cases', + name: 'System action: .cases', + config: {}, + secrets: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ]); + expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.cases')).toBe(true); + }); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ba4fec697b0c7b..bc29c3e9ad2467 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -60,7 +60,7 @@ import { import { Services, ActionType, - PreConfiguredAction, + InMemoryConnector, ActionTypeConfig, ActionTypeSecrets, ActionTypeParams, @@ -106,6 +106,7 @@ import { UnsecuredActionsClient, } from './unsecured_actions_client/unsecured_actions_client'; import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; +import { createSystemConnectors } from './create_system_actions'; export interface PluginSetupContract { registerType< @@ -147,7 +148,7 @@ export interface PluginStartContract { getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; - preconfiguredActions: PreConfiguredAction[]; + inMemoryConnectors: InMemoryConnector[]; getUnsecuredActionsClient(): IUnsecuredActionsClient; @@ -200,7 +201,7 @@ export class ActionsPlugin implements Plugin()) ); this.telemetryLogger = initContext.logger.get('usage'); - this.preconfiguredActions = []; + this.inMemoryConnectors = []; this.inMemoryMetrics = new InMemoryMetrics(initContext.logger.get('in_memory_metrics')); } @@ -244,7 +245,7 @@ export class ActionsPlugin implements Plugin { - return !!this.preconfiguredActions.find( - (preconfigured) => preconfigured.id === connectorId + return !!this.inMemoryConnectors.find( + (inMemoryConnector) => + inMemoryConnector.isPreconfigured && inMemoryConnector.id === connectorId ); }, getSubActionConnectorClass: () => SubActionConnector, @@ -384,7 +386,6 @@ export class ActionsPlugin implements Plugin @@ -416,7 +427,7 @@ export class ActionsPlugin implements Plugin renderActionParameterTemplates(actionTypeRegistry, ...args), }; @@ -589,13 +600,20 @@ export class ActionsPlugin implements Plugin this.inMemoryConnectors; + + private setSystemActions = () => { + const systemConnectors = createSystemConnectors(this.actionTypeRegistry?.list() ?? []); + this.inMemoryConnectors = [...this.inMemoryConnectors, ...systemConnectors]; + }; + private createRouteHandlerContext = ( core: CoreSetup ): IContextProvider => { const { actionTypeRegistry, isESOCanEncrypt, - preconfiguredActions, + getInMemoryConnectors, actionExecutor, instantiateAuthorization, security, @@ -606,7 +624,9 @@ export class ActionsPlugin implements Plugin { @@ -625,7 +645,7 @@ export class ActionsPlugin implements Plugin { +export function getAlertHistoryEsIndex(): Readonly { return Object.freeze({ name: i18n.translate('xpack.actions.alertHistoryEsIndexConnector.name', { defaultMessage: 'Alert history Elasticsearch index', diff --git a/x-pack/plugins/actions/server/routes/connector_types.test.ts b/x-pack/plugins/actions/server/routes/connector_types.test.ts index 8a4639cf3f4a2a..51fa8c1630d5fb 100644 --- a/x-pack/plugins/actions/server/routes/connector_types.test.ts +++ b/x-pack/plugins/actions/server/routes/connector_types.test.ts @@ -42,6 +42,7 @@ describe('connectorTypesRoute', () => { enabledInLicense: true, minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; @@ -57,6 +58,7 @@ describe('connectorTypesRoute', () => { "enabled_in_config": true, "enabled_in_license": true, "id": "1", + "is_system_action_type": false, "minimum_license_required": "gold", "name": "name", "supported_feature_ids": Array [ @@ -77,6 +79,7 @@ describe('connectorTypesRoute', () => { enabled_in_license: true, supported_feature_ids: ['alerting'], minimum_license_required: 'gold', + is_system_action_type: false, }, ], }); @@ -101,6 +104,7 @@ describe('connectorTypesRoute', () => { enabledInLicense: true, supportedFeatureIds: ['alerting'], minimumLicenseRequired: 'gold' as LicenseType, + isSystemActionType: false, }, ]; @@ -124,6 +128,7 @@ describe('connectorTypesRoute', () => { "enabled_in_config": true, "enabled_in_license": true, "id": "1", + "is_system_action_type": false, "minimum_license_required": "gold", "name": "name", "supported_feature_ids": Array [ @@ -151,6 +156,7 @@ describe('connectorTypesRoute', () => { enabled_in_license: true, supported_feature_ids: ['alerting'], minimum_license_required: 'gold', + is_system_action_type: false, }, ], }); @@ -175,6 +181,7 @@ describe('connectorTypesRoute', () => { enabledInLicense: true, supportedFeatureIds: ['alerting'], minimumLicenseRequired: 'gold' as LicenseType, + isSystemActionType: false, }, ]; @@ -217,6 +224,7 @@ describe('connectorTypesRoute', () => { enabledInLicense: true, supportedFeatureIds: ['alerting'], minimumLicenseRequired: 'gold' as LicenseType, + isSystemActionType: false, }, ]; diff --git a/x-pack/plugins/actions/server/routes/connector_types.ts b/x-pack/plugins/actions/server/routes/connector_types.ts index 6d014a4eba61c0..39c24b6c178c8e 100644 --- a/x-pack/plugins/actions/server/routes/connector_types.ts +++ b/x-pack/plugins/actions/server/routes/connector_types.ts @@ -23,6 +23,7 @@ const rewriteBodyRes: RewriteResponseCase = (results) => { enabledInLicense, minimumLicenseRequired, supportedFeatureIds, + isSystemActionType, ...res }) => ({ ...res, @@ -30,6 +31,7 @@ const rewriteBodyRes: RewriteResponseCase = (results) => { enabled_in_license: enabledInLicense, minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, + is_system_action_type: isSystemActionType, }) ); }; diff --git a/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts index 045ad01719e934..4674bbb9936a69 100644 --- a/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts @@ -50,6 +50,7 @@ describe('listActionTypesRoute', () => { enabledInLicense: true, minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; @@ -65,6 +66,7 @@ describe('listActionTypesRoute', () => { "enabledInConfig": true, "enabledInLicense": true, "id": "1", + "isSystemActionType": false, "minimumLicenseRequired": "gold", "name": "name", "supportedFeatureIds": Array [ @@ -99,6 +101,7 @@ describe('listActionTypesRoute', () => { enabledInLicense: true, minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; @@ -141,6 +144,7 @@ describe('listActionTypesRoute', () => { enabledInLicense: true, minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts index 7ef21c8fc84bd3..798bcbbaacd25a 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts @@ -6,10 +6,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { - getActionTaskParamsMigrations, - isPreconfiguredAction, -} from './action_task_params_migrations'; +import { getActionTaskParamsMigrations, isInMemoryAction } from './action_task_params_migrations'; import { ActionTaskParams } from '../types'; import { SavedObjectReference, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; @@ -19,7 +16,7 @@ import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; const context = migrationMocks.createContext(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); -const preconfiguredActions = [ +const inMemoryConnectors = [ { actionTypeId: 'foo', config: {}, @@ -39,9 +36,9 @@ describe('successful migrations', () => { }); describe('7.16.0', () => { - test('adds actionId to references array if actionId is not preconfigured', () => { + test('adds actionId to references array if actionId is not in-memory', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData(); const migratedActionTaskParam = migration716(actionTaskParam, context); @@ -57,9 +54,9 @@ describe('successful migrations', () => { }); }); - test('does not add actionId to references array if actionId is preconfigured', () => { + test('does not add actionId to references array if actionId is in-memory', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ actionId: 'my-slack1' }); const migratedActionTaskParam = migration716(actionTaskParam, context); @@ -71,7 +68,7 @@ describe('successful migrations', () => { test('handles empty relatedSavedObjects array', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ relatedSavedObjects: [] }); const migratedActionTaskParam = migration716(actionTaskParam, context); @@ -93,7 +90,7 @@ describe('successful migrations', () => { test('adds actionId and relatedSavedObjects to references array', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ relatedSavedObjects: [ @@ -134,9 +131,9 @@ describe('successful migrations', () => { }); }); - test('only adds relatedSavedObjects to references array if action is preconfigured', () => { + test('only adds relatedSavedObjects to references array if action is in-memory', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ actionId: 'my-slack1', @@ -175,7 +172,7 @@ describe('successful migrations', () => { test('adds actionId and multiple relatedSavedObjects to references array', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ relatedSavedObjects: [ @@ -233,7 +230,7 @@ describe('successful migrations', () => { test('does not overwrite existing references', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData( { @@ -290,7 +287,7 @@ describe('successful migrations', () => { test('does not overwrite existing references if relatedSavedObjects is undefined', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({}, [ { @@ -319,7 +316,7 @@ describe('successful migrations', () => { test('does not overwrite existing references if relatedSavedObjects is empty', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData({ relatedSavedObjects: [] }, [ { @@ -373,7 +370,7 @@ describe('handles errors during migrations', () => { describe('7.16.0 throws if migration fails', () => { test('should show the proper exception', () => { const migration716 = SavedObjectsUtils.getMigrationFunction( - getActionTaskParamsMigrations(encryptedSavedObjectsSetup, preconfiguredActions)['7.16.0'] + getActionTaskParamsMigrations(encryptedSavedObjectsSetup, inMemoryConnectors)['7.16.0'] ); const actionTaskParam = getMockData(); expect(() => { @@ -391,15 +388,15 @@ describe('handles errors during migrations', () => { }); }); -describe('isPreconfiguredAction()', () => { - test('returns true if actionId is preconfigured action', () => { - expect( - isPreconfiguredAction(getMockData({ actionId: 'my-slack1' }), preconfiguredActions) - ).toEqual(true); +describe('isInMemoryAction()', () => { + test('returns true if actionId is in-memory action', () => { + expect(isInMemoryAction(getMockData({ actionId: 'my-slack1' }), inMemoryConnectors)).toEqual( + true + ); }); - test('returns false if actionId is not preconfigured action', () => { - expect(isPreconfiguredAction(getMockData(), preconfiguredActions)).toEqual(false); + test('returns false if actionId is not in-memory action', () => { + expect(isInMemoryAction(getMockData(), inMemoryConnectors)).toEqual(false); }); }); diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts index b0778d680e70eb..3ef9bb0299d7ef 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts @@ -15,7 +15,7 @@ import { } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import type { IsMigrationNeededPredicate } from '@kbn/encrypted-saved-objects-plugin/server'; -import { ActionTaskParams, PreConfiguredAction } from '../types'; +import { ActionTaskParams, InMemoryConnector } from '../types'; import { RelatedSavedObjects } from '../lib/related_saved_objects'; interface ActionTaskParamsLogMeta extends LogMeta { @@ -40,12 +40,12 @@ function createEsoMigration( export function getActionTaskParamsMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - preconfiguredActions: PreConfiguredAction[] + inMemoryConnectors: InMemoryConnector[] ): SavedObjectMigrationMap { const migrationActionTaskParamsSixteen = createEsoMigration( encryptedSavedObjects, (doc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(getUseSavedObjectReferencesFn(preconfiguredActions)) + pipeMigrations(getUseSavedObjectReferencesFn(inMemoryConnectors)) ); const migrationActionsTaskParams800 = createEsoMigration( @@ -86,22 +86,22 @@ function executeMigrationWithErrorHandling( }; } -export function isPreconfiguredAction( +export function isInMemoryAction( doc: SavedObjectUnsanitizedDoc, - preconfiguredActions: PreConfiguredAction[] + inMemoryConnectors: InMemoryConnector[] ): boolean { - return !!preconfiguredActions.find((action) => action.id === doc.attributes.actionId); + return !!inMemoryConnectors.find((action) => action.id === doc.attributes.actionId); } -function getUseSavedObjectReferencesFn(preconfiguredActions: PreConfiguredAction[]) { +function getUseSavedObjectReferencesFn(inMemoryConnectors: InMemoryConnector[]) { return (doc: SavedObjectUnsanitizedDoc) => { - return useSavedObjectReferences(doc, preconfiguredActions); + return useSavedObjectReferences(doc, inMemoryConnectors); }; } function useSavedObjectReferences( doc: SavedObjectUnsanitizedDoc, - preconfiguredActions: PreConfiguredAction[] + inMemoryConnectors: InMemoryConnector[] ): SavedObjectUnsanitizedDoc { const { attributes: { actionId, relatedSavedObjects }, @@ -111,7 +111,7 @@ function useSavedObjectReferences( const newReferences: SavedObjectReference[] = []; const relatedSavedObjectRefs: RelatedSavedObjects = []; - if (!isPreconfiguredAction(doc, preconfiguredActions)) { + if (!isInMemoryAction(doc, inMemoryConnectors)) { newReferences.push({ id: actionId, name: 'actionRef', diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 5b38062265196d..d5125ecc134cd8 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -16,7 +16,7 @@ import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-serve import { actionMappings, actionTaskParamsMappings, connectorTokenMappings } from './mappings'; import { getActionsMigrations } from './actions_migrations'; import { getActionTaskParamsMigrations } from './action_task_params_migrations'; -import { PreConfiguredAction, RawAction } from '../types'; +import { InMemoryConnector, RawAction } from '../types'; import { getImportWarnings } from './get_import_warnings'; import { transformConnectorsForExport } from './transform_connectors_for_export'; import { ActionTypeRegistry } from '../action_type_registry'; @@ -31,7 +31,7 @@ export function setupSavedObjects( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, actionTypeRegistry: ActionTypeRegistry, taskManagerIndex: string, - preconfiguredActions: PreConfiguredAction[] + inMemoryConnectors: InMemoryConnector[] ) { savedObjects.registerType({ name: ACTION_SAVED_OBJECT_TYPE, @@ -79,7 +79,7 @@ export function setupSavedObjects( namespaceType: 'multiple-isolated', convertToMultiNamespaceTypeVersion: '8.0.0', mappings: actionTaskParamsMappings, - migrations: getActionTaskParamsMigrations(encryptedSavedObjects, preconfiguredActions), + migrations: getActionTaskParamsMigrations(encryptedSavedObjects, inMemoryConnectors), excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestIdleActionTask = await getOldestIdleActionTask( readonlyEsClient, diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 54f5f8327be65a..d51af5a37f8125 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -84,7 +84,7 @@ export interface ActionResult extends ActionResult { @@ -140,7 +140,7 @@ export interface ActionType< secrets: ValidatorType; connector?: (config: Config, secrets: Secrets) => string | null; }; - isSystemAction?: boolean; + isSystemActionType?: boolean; renderParameterTemplates?: RenderParameterTemplates; executor: ExecutorType; } diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index 6e2b4ebbfff80d..649f1109311af3 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -13,13 +13,13 @@ import { parseActionRunOutcomeByConnectorTypesBucket, } from './lib/parse_connector_type_bucket'; import { AlertHistoryEsIndexConnectorId } from '../../common'; -import { ActionResult, PreConfiguredAction } from '../types'; +import { ActionResult, InMemoryConnector } from '../types'; export async function getTotalCount( esClient: ElasticsearchClient, kibanaIndex: string, logger: Logger, - preconfiguredActions?: PreConfiguredAction[] + inMemoryConnectors?: InMemoryConnector[] ) { const scriptedMetric = { scripted_metric: { @@ -74,9 +74,9 @@ export async function getTotalCount( }, {} ); - if (preconfiguredActions && preconfiguredActions.length) { - for (const preconfiguredAction of preconfiguredActions) { - const actionTypeId = replaceFirstAndLastDotSymbols(preconfiguredAction.actionTypeId); + if (inMemoryConnectors && inMemoryConnectors.length) { + for (const inMemoryConnector of inMemoryConnectors) { + const actionTypeId = replaceFirstAndLastDotSymbols(inMemoryConnector.actionTypeId); countByType[actionTypeId] = countByType[actionTypeId] || 0; countByType[actionTypeId]++; } @@ -87,7 +87,7 @@ export async function getTotalCount( Object.keys(aggs).reduce( (total: number, key: string) => parseInt(aggs[key], 10) + total, 0 - ) + (preconfiguredActions?.length ?? 0), + ) + (inMemoryConnectors?.length ?? 0), countByType, }; } catch (err) { @@ -109,7 +109,7 @@ export async function getInUseTotalCount( kibanaIndex: string, logger: Logger, referenceType?: string, - preconfiguredActions?: PreConfiguredAction[] + inMemoryConnectors?: InMemoryConnector[] ): Promise<{ hasErrors: boolean; errorMessage?: string; @@ -363,9 +363,9 @@ export async function getInUseTotalCount( if (actionRef === `preconfigured:${AlertHistoryEsIndexConnectorId}`) { preconfiguredAlertHistoryConnectors++; } - if (preconfiguredActions && actionTypeId === '__email') { + if (inMemoryConnectors && actionTypeId === '__email') { const preconfiguredConnectorId = actionRef.split(':')[1]; - const service = (preconfiguredActions.find( + const service = (inMemoryConnectors.find( (preconfConnector) => preconfConnector.id === preconfiguredConnectorId )?.config?.service ?? 'other') as string; const currentCount = diff --git a/x-pack/plugins/actions/server/usage/task.ts b/x-pack/plugins/actions/server/usage/task.ts index 7faf443019c6b7..5e885e0fae5ca9 100644 --- a/x-pack/plugins/actions/server/usage/task.ts +++ b/x-pack/plugins/actions/server/usage/task.ts @@ -12,7 +12,7 @@ import { TaskManagerStartContract, IntervalSchedule, } from '@kbn/task-manager-plugin/server'; -import { PreConfiguredAction } from '../types'; +import { InMemoryConnector } from '../types'; import { getTotalCount, getInUseTotalCount, getExecutionsPerDayCount } from './actions_telemetry'; export const TELEMETRY_TASK_TYPE = 'actions_telemetry'; @@ -24,10 +24,10 @@ export function initializeActionsTelemetry( logger: Logger, taskManager: TaskManagerSetupContract, core: CoreSetup, - preconfiguredActions: PreConfiguredAction[], + getInMemoryConnectors: () => InMemoryConnector[], eventLogIndex: string ) { - registerActionsTelemetryTask(logger, taskManager, core, preconfiguredActions, eventLogIndex); + registerActionsTelemetryTask(logger, taskManager, core, getInMemoryConnectors, eventLogIndex); } export function scheduleActionsTelemetry(logger: Logger, taskManager: TaskManagerStartContract) { @@ -38,14 +38,14 @@ function registerActionsTelemetryTask( logger: Logger, taskManager: TaskManagerSetupContract, core: CoreSetup, - preconfiguredActions: PreConfiguredAction[], + getInMemoryConnectors: () => InMemoryConnector[], eventLogIndex: string ) { taskManager.registerTaskDefinitions({ [TELEMETRY_TASK_TYPE]: { title: 'Actions usage fetch task', timeout: '5m', - createTaskRunner: telemetryTaskRunner(logger, core, preconfiguredActions, eventLogIndex), + createTaskRunner: telemetryTaskRunner(logger, core, getInMemoryConnectors, eventLogIndex), }, }); } @@ -67,9 +67,17 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner( logger: Logger, core: CoreSetup, - preconfiguredActions: PreConfiguredAction[], + getInMemoryConnectors: () => InMemoryConnector[], eventLogIndex: string ) { + /** + * Filter out system actions from the + * inMemoryConnectors list. + */ + const inMemoryConnectors = getInMemoryConnectors().filter( + (inMemoryConnector) => inMemoryConnector.isPreconfigured + ); + return ({ taskInstance }: RunContext) => { const { state } = taskInstance; const getEsClient = () => @@ -89,8 +97,8 @@ export function telemetryTaskRunner( const actionIndex = await getActionIndex(); const esClient = await getEsClient(); return Promise.all([ - getTotalCount(esClient, actionIndex, logger, preconfiguredActions), - getInUseTotalCount(esClient, actionIndex, logger, undefined, preconfiguredActions), + getTotalCount(esClient, actionIndex, logger, inMemoryConnectors), + getInUseTotalCount(esClient, actionIndex, logger, undefined, inMemoryConnectors), getExecutionsPerDayCount(esClient, eventLogIndex, logger), ]).then(([totalAggegations, totalInUse, totalExecutionsPerDay]) => { const hasErrors = diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index 2eacd29a39db61..90c7ec1dc095ca 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -354,7 +354,7 @@ describe('Execution Handler', () => { }); test('throw error message when action type is disabled', async () => { - mockActionsPlugin.preconfiguredActions = []; + mockActionsPlugin.inMemoryConnectors = []; mockActionsPlugin.isActionExecutable.mockReturnValue(false); mockActionsPlugin.isActionTypeEnabled.mockReturnValue(false); const executionHandler = new ExecutionHandler( diff --git a/x-pack/plugins/cases/public/common/mock/connectors.ts b/x-pack/plugins/cases/public/common/mock/connectors.ts index 3485af438a4d9f..f1ba88deedd14e 100644 --- a/x-pack/plugins/cases/public/common/mock/connectors.ts +++ b/x-pack/plugins/cases/public/common/mock/connectors.ts @@ -79,6 +79,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, { id: '.index', @@ -88,6 +89,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, { id: '.servicenow', @@ -97,6 +99,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, { id: '.jira', @@ -106,6 +109,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, { id: '.resilient', @@ -115,6 +119,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, { id: '.servicenow-sir', @@ -124,6 +129,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, ]; diff --git a/x-pack/plugins/cases/server/client/configure/client.test.ts b/x-pack/plugins/cases/server/client/configure/client.test.ts index 43e19a799b5e66..f87cfa40c4765c 100644 --- a/x-pack/plugins/cases/server/client/configure/client.test.ts +++ b/x-pack/plugins/cases/server/client/configure/client.test.ts @@ -34,6 +34,7 @@ describe('client', () => { enabledInLicense: true, minimumLicenseRequired: 'basic' as const, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, { id: '.servicenow', @@ -43,6 +44,7 @@ describe('client', () => { enabledInLicense: true, minimumLicenseRequired: 'basic' as const, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, { id: '.unsupported', @@ -52,6 +54,7 @@ describe('client', () => { enabledInLicense: true, minimumLicenseRequired: 'basic' as const, supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, { id: '.swimlane', @@ -61,6 +64,7 @@ describe('client', () => { enabledInLicense: false, minimumLicenseRequired: 'basic' as const, supportedFeatureIds: ['alerting', 'cases'], + isSystemActionType: false, }, ]; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts index b7ae7884b178af..da8855f531ef9d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts @@ -85,6 +85,7 @@ export const fetchActionTypes = async (): Promise => { enabled_in_license: enabledInLicense, minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, + is_system_action_type: isSystemActionType, ...res }: AsApiContract) => ({ ...res, @@ -92,6 +93,7 @@ export const fetchActionTypes = async (): Promise => { enabledInLicense, minimumLicenseRequired, supportedFeatureIds, + isSystemActionType, }) ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts index 69b8975a9768f2..182abc1507a346 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts @@ -24,6 +24,7 @@ describe('loadActionTypes', () => { enabled_in_license: true, supported_feature_ids: ['alerting'], minimum_license_required: 'basic', + is_system_action_type: false, }, ]; http.get.mockResolvedValueOnce(apiResponseValue); @@ -37,6 +38,7 @@ describe('loadActionTypes', () => { enabledInLicense: true, supportedFeatureIds: ['alerting'], minimumLicenseRequired: 'basic', + isSystemActionType: false, }, ]; @@ -59,6 +61,7 @@ describe('loadActionTypes', () => { enabled_in_license: true, supported_feature_ids: ['alerting'], minimum_license_required: 'basic', + is_system_action_type: false, }, ]; http.get.mockResolvedValueOnce(apiResponseValue); @@ -72,6 +75,7 @@ describe('loadActionTypes', () => { enabledInLicense: true, supportedFeatureIds: ['alerting'], minimumLicenseRequired: 'basic', + isSystemActionType: false, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts index 191ae96acc8b66..be16cfc65309e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts @@ -19,12 +19,14 @@ const rewriteBodyReq: RewriteRequestCase = ({ enabled_in_license: enabledInLicense, minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, + is_system_action_type: isSystemActionType, ...res }: AsApiContract) => ({ enabledInConfig, enabledInLicense, minimumLicenseRequired, supportedFeatureIds, + isSystemActionType, ...res, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts index ffc6594d596518..8c9e71bb7d03b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts @@ -18,6 +18,7 @@ test('should sort enabled action types first', async () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }, { id: '2', @@ -27,6 +28,7 @@ test('should sort enabled action types first', async () => { enabled: false, enabledInConfig: true, enabledInLicense: false, + isSystemActionType: false, }, { id: '3', @@ -36,6 +38,7 @@ test('should sort enabled action types first', async () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }, { id: '4', @@ -45,6 +48,7 @@ test('should sort enabled action types first', async () => { enabled: true, enabledInConfig: false, enabledInLicense: true, + isSystemActionType: false, }, ]; const result = [...actionTypes].sort(actionTypeCompare); @@ -64,6 +68,7 @@ test('should sort by name when all enabled', async () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }, { id: '2', @@ -73,6 +78,7 @@ test('should sort by name when all enabled', async () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }, { id: '3', @@ -82,6 +88,7 @@ test('should sort by name when all enabled', async () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }, { id: '4', @@ -91,6 +98,7 @@ test('should sort by name when all enabled', async () => { enabled: true, enabledInConfig: false, enabledInLicense: true, + isSystemActionType: false, }, ]; const result = [...actionTypes].sort(actionTypeCompare); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index e2dc91214e783e..2cdd25267de0eb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -29,6 +29,7 @@ describe('checkActionTypeEnabled', () => { enabled: true, enabledInConfig: true, enabledInLicense: true, + isSystemActionType: false, }; expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { @@ -46,6 +47,7 @@ describe('checkActionTypeEnabled', () => { enabled: false, enabledInConfig: true, enabledInLicense: false, + isSystemActionType: false, }; expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { @@ -81,6 +83,7 @@ describe('checkActionTypeEnabled', () => { enabled: false, enabledInConfig: false, enabledInLicense: true, + isSystemActionType: false, }; expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { @@ -127,6 +130,7 @@ describe('checkActionFormActionTypeEnabled', () => { enabled: true, enabledInConfig: false, enabledInLicense: true, + isSystemActionType: false, }; expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) @@ -146,6 +150,7 @@ describe('checkActionFormActionTypeEnabled', () => { enabled: true, enabledInConfig: false, enabledInLicense: true, + isSystemActionType: false, }; expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) .toMatchInlineSnapshot(` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index 46dafc3c4fc033..4cfe807ecd7284 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -605,6 +605,7 @@ function getActionTypeForm({ enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, '.server-log': { id: '.server-log', @@ -614,6 +615,7 @@ function getActionTypeForm({ enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index fcc80ace505ffe..6efbf023e76d61 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -58,6 +58,7 @@ describe('connector_add_modal', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }; const wrapper = mountWithIntl( @@ -100,6 +101,7 @@ describe('connector_add_modal', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }; const wrapper = mountWithIntl( { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }; const wrapper = mountWithIntl( { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index b2edd76a394015..3bd45741714a01 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -216,6 +216,7 @@ describe('rule_details', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; @@ -259,6 +260,7 @@ describe('rule_details', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, { id: '.email', @@ -268,6 +270,7 @@ describe('rule_details', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; @@ -338,6 +341,7 @@ describe('rule_details', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; ruleTypeRegistry.has.mockReturnValue(true); @@ -468,6 +472,7 @@ describe('rule_details', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', supportedFeatureIds: ['alerting'], + isSystemActionType: false, }, ]; ruleTypeRegistry.has.mockReturnValue(true); diff --git a/x-pack/plugins/uptime/public/legacy_uptime/pages/settings.test.tsx b/x-pack/plugins/uptime/public/legacy_uptime/pages/settings.test.tsx index 8a87ff2969974d..5c8fb95448fca0 100644 --- a/x-pack/plugins/uptime/public/legacy_uptime/pages/settings.test.tsx +++ b/x-pack/plugins/uptime/public/legacy_uptime/pages/settings.test.tsx @@ -24,6 +24,7 @@ describe('settings', () => { minimumLicenseRequired: 'gold', name: 'Slack', supportedFeatureIds: ['uptime'], + isSystemActionType: false, }, ]); }); diff --git a/x-pack/plugins/uptime/public/legacy_uptime/state/api/alerts.ts b/x-pack/plugins/uptime/public/legacy_uptime/state/api/alerts.ts index 8c623e914516b5..9ca6a386997ec3 100644 --- a/x-pack/plugins/uptime/public/legacy_uptime/state/api/alerts.ts +++ b/x-pack/plugins/uptime/public/legacy_uptime/state/api/alerts.ts @@ -172,6 +172,7 @@ export const fetchActionTypes = async (): Promise => { enabled_in_license: enabledInLicense, minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, + is_system_action_type: isSystemActionType, ...res }: AsApiContract) => ({ ...res, @@ -179,6 +180,7 @@ export const fetchActionTypes = async (): Promise => { enabledInLicense, minimumLicenseRequired, supportedFeatureIds, + isSystemActionType, }) ); }; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 851bc653d62525..3e48a3b273019d 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -64,6 +64,7 @@ const enabledActionTypes = [ 'test.throw', 'test.excluded', 'test.capped', + 'test.system-action', ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts index d04fb52b7637a0..cddf1002050586 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts @@ -74,6 +74,7 @@ export function defineActionTypes( actions.registerType(getNoAttemptsRateLimitedActionType()); actions.registerType(getAuthorizationActionType(core)); actions.registerType(getExcludedActionType()); + actions.registerType(getSystemActionType()); /** Sub action framework */ @@ -399,3 +400,29 @@ function getExcludedActionType() { }; return result; } + +function getSystemActionType() { + const result: ActionType<{}, {}, {}> = { + id: 'test.system-action', + name: 'Test system action', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + params: { + schema: schema.any(), + }, + config: { + schema: schema.any(), + }, + secrets: { + schema: schema.any(), + }, + }, + isSystemActionType: true, + async executor({ config, secrets, params, services, actionId }) { + return { status: 'ok', actionId }; + }, + }; + + return result; +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts index 4c9db37fe35435..017fd3e45999bd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts @@ -319,6 +319,83 @@ export default function createActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't create a preconfigured action with the same id as an existing one`, async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/actions/connector/custom-system-abc-connector`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'system-abc-action-type', + config: {}, + secrets: {}, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to create a "system-abc-action-type" action', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'This custom-system-abc-connector already exists in a preconfigured action.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't create a system action`, async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My system action', + connector_type_id: 'test.system-action', + config: {}, + secrets: {}, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to create a "test.system-action" action', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'System action creation is forbidden. Action type: test.system-action.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts index b0cfffdd2f464a..b5b11036a3dfd8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts @@ -146,7 +146,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { } }); - it(`shouldn't delete action from preconfigured list`, async () => { + it(`shouldn't delete preconfigured action`, async () => { const response = await supertestWithoutAuth .delete(`${getUrlPrefix(space.id)}/api/actions/connector/my-slack1`) .auth(user.username, user.password) @@ -177,6 +177,41 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't delete system action`, async () => { + const response = await supertestWithoutAuth + .delete( + `${getUrlPrefix(space.id)}/api/actions/connector/system-connector-test.system-action` + ) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to delete actions', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'System action system-connector-test.system-action is not allowed to delete.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts index 3b04af22d2e96c..42cfce82cb2e1a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts @@ -160,6 +160,43 @@ export default function getActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle get system action request appropriately', async () => { + const response = await supertestWithoutAuth + .get( + `${getUrlPrefix(space.id)}/api/actions/connector/system-connector-test.system-action` + ) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to get actions', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + id: 'system-connector-test.system-action', + connector_type_id: 'test.system-action', + name: 'System action: test.system-action', + is_preconfigured: false, + is_system_action: true, + is_deprecated: false, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index 37475d7f191cad..bc3444b5a32b3e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -125,6 +125,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + connector_type_id: 'test.system-action', + id: 'system-connector-test.system-action', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'System action: test.system-action', + referenced_by_count: 0, + }, { id: 'custom-system-abc-connector', is_preconfigured: true, @@ -285,6 +294,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + connector_type_id: 'test.system-action', + id: 'system-connector-test.system-action', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'System action: test.system-action', + referenced_by_count: 0, + }, { id: 'custom-system-abc-connector', is_preconfigured: true, @@ -408,6 +426,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + connector_type_id: 'test.system-action', + id: 'system-connector-test.system-action', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'System action: test.system-action', + referenced_by_count: 0, + }, { id: 'custom-system-abc-connector', is_preconfigured: true, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts index 07d2e6064bc0bc..7c3c00534f11d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts @@ -312,7 +312,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { } }); - it(`shouldn't update action from preconfigured list`, async () => { + it(`shouldn't update a preconfigured action`, async () => { const response = await supertestWithoutAuth .put(`${getUrlPrefix(space.id)}/api/actions/connector/custom-system-abc-connector`) .auth(user.username, user.password) @@ -345,7 +345,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + message: `Preconfigured action custom-system-abc-connector can not be updated.`, }); break; default: @@ -387,6 +387,49 @@ export default function updateActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't update a system action`, async () => { + const response = await supertestWithoutAuth + .put( + `${getUrlPrefix(space.id)}/api/actions/connector/system-connector-test.system-action` + ) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to update actions', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'System action system-connector-test.system-action can not be updated.', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index a7239343b2c610..6941aa6f180255 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -80,6 +80,40 @@ export default function createActionTests({ getService }: FtrProviderContext) { }); }); + it(`shouldn't create a preconfigured action with the same id as an existing one`, async () => { + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/custom-system-abc-connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'system-abc-action-type', + config: {}, + secrets: {}, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'This custom-system-abc-connector already exists in a preconfigured action.', + }); + }); + + it(`shouldn't create a system action`, async () => { + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My system action', + connector_type_id: 'test.system-action', + config: {}, + secrets: {}, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'System action creation is forbidden. Action type: test.system-action.', + }); + }); + describe('legacy', () => { it('should handle create action request appropriately', async () => { const response = await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts index 66ae047e5151b0..5d3a5f0c32ecb7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts @@ -78,7 +78,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { }); }); - it(`shouldn't delete action from preconfigured list`, async () => { + it(`shouldn't delete a preconfigured action`, async () => { await supertest .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-slack1`) .set('kbn-xsrf', 'foo') @@ -89,6 +89,21 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { }); }); + it(`shouldn't delete a system action`, async () => { + await supertest + .delete( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/connector/system-connector-test.system-action` + ) + .set('kbn-xsrf', 'foo') + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'System action system-connector-test.system-action is not allowed to delete.', + }); + }); + describe('legacy', () => { it('should handle delete action request appropriately', async () => { const { body: createdAction } = await supertest @@ -150,7 +165,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { }); }); - it(`shouldn't delete action from preconfigured list`, async () => { + it(`shouldn't delete a preconfigured action`, async () => { await supertest .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) .set('kbn-xsrf', 'foo') @@ -160,6 +175,21 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { message: `Preconfigured action my-slack1 is not allowed to delete.`, }); }); + + it(`shouldn't delete a system action`, async () => { + await supertest + .delete( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/action/system-connector-test.system-action` + ) + .set('kbn-xsrf', 'foo') + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'System action system-connector-test.system-action is not allowed to delete.', + }); + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index 68d4512145118a..dc99597a901e12 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -77,7 +77,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { }); }); - it('should handle get action request from preconfigured list', async () => { + it('should handle get a preconfigured connector', async () => { await supertest .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-slack1`) .expect(200, { @@ -90,7 +90,24 @@ export default function getActionTests({ getService }: FtrProviderContext) { }); }); - it('should handle get action request for deprecated connectors from preconfigured list', async () => { + it('should handle get a system connector', async () => { + await supertest + .get( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/connector/system-connector-test.system-action` + ) + .expect(200, { + id: 'system-connector-test.system-action', + connector_type_id: 'test.system-action', + name: 'System action: test.system-action', + is_preconfigured: false, + is_system_action: true, + is_deprecated: false, + }); + }); + + it('should handle get a deprecated connector', async () => { await supertest .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-deprecated-servicenow`) .expect(200, { @@ -176,7 +193,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { }); }); - it('should handle get action request from preconfigured list', async () => { + it('should handle get a preconfigured connector', async () => { await supertest .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) .expect(200, { @@ -188,6 +205,23 @@ export default function getActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', }); }); + + it('should handle get a system connector', async () => { + await supertest + .get( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/action/system-connector-test.system-action` + ) + .expect(200, { + id: 'system-connector-test.system-action', + actionTypeId: 'test.system-action', + name: 'System action: test.system-action', + isPreconfigured: false, + isSystemAction: true, + isDeprecated: false, + }); + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index d4a43da31894e3..2366c392c4e4d0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -114,6 +114,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + connector_type_id: 'test.system-action', + id: 'system-connector-test.system-action', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'System action: test.system-action', + referenced_by_count: 0, + }, { id: 'custom-system-abc-connector', is_preconfigured: true, @@ -226,6 +235,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + connector_type_id: 'test.system-action', + id: 'system-connector-test.system-action', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'System action: test.system-action', + referenced_by_count: 0, + }, { id: 'custom-system-abc-connector', is_preconfigured: true, @@ -352,6 +370,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referencedByCount: 0, }, + { + actionTypeId: 'test.system-action', + id: 'system-connector-test.system-action', + isDeprecated: false, + isPreconfigured: false, + isSystemAction: true, + name: 'System action: test.system-action', + referencedByCount: 0, + }, { id: 'custom-system-abc-connector', isPreconfigured: true, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts index 9a5719b7fa7000..409e56e7b1fd85 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts @@ -149,7 +149,7 @@ export default function createUnsecuredActionTests({ getService }: FtrProviderCo ); }); - it('should not allow scheduling action from non preconfigured connectors', async () => { + it('should not allow scheduling action from non in-memory connectors', async () => { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -184,7 +184,7 @@ export default function createUnsecuredActionTests({ getService }: FtrProviderCo .expect(200); expect(result.status).to.eql('error'); expect(result.error).to.eql( - `Error: ${connectorId} are not preconfigured connectors and can't be scheduled for unsecured actions execution` + `Error: ${connectorId} are not in-memory connectors and can't be scheduled for unsecured actions execution` ); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index d6f41e3037ad20..d64ccc15c49994 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -106,7 +106,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }); }); - it(`shouldn't update action from preconfigured list`, async () => { + it(`shouldn't update a preconfigured connector`, async () => { await supertest .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/custom-system-abc-connector`) .set('kbn-xsrf', 'foo') @@ -122,7 +122,31 @@ export default function updateActionTests({ getService }: FtrProviderContext) { .expect(400, { statusCode: 400, error: 'Bad Request', - message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + message: `Preconfigured action custom-system-abc-connector can not be updated.`, + }); + }); + + it(`shouldn't update a system connector`, async () => { + await supertest + .put( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/connector/system-connector-test.system-action` + ) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'System action system-connector-test.system-action can not be updated.', }); }); @@ -270,7 +294,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }); }); - it(`shouldn't update action from preconfigured list`, async () => { + it(`shouldn't update a preconfigured connector`, async () => { await supertest .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/custom-system-abc-connector`) .set('kbn-xsrf', 'foo') @@ -286,7 +310,31 @@ export default function updateActionTests({ getService }: FtrProviderContext) { .expect(400, { statusCode: 400, error: 'Bad Request', - message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + message: `Preconfigured action custom-system-abc-connector can not be updated.`, + }); + }); + + it(`shouldn't update a system connector`, async () => { + await supertest + .put( + `${getUrlPrefix( + Spaces.space1.id + )}/api/actions/action/system-connector-test.system-action` + ) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'System action system-connector-test.system-action can not be updated.', }); });