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 d4100537fa6b8e..4cacba6dc880ab 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -177,6 +177,35 @@ describe('execute()', () => { ); }); + test('throws when isMissingSecrets is true for connector', async () => { + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + isESOCanEncrypt: true, + actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + name: 'mock-action', + isMissingSecrets: true, + actionTypeId: 'mock-action', + }, + references: [], + }); + await expect( + executeFn(savedObjectsClient, { + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to execute action because no secrets are defined for the \\"mock-action\\" connector."` + ); + }); + test('should ensure action type is enabled', async () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 025b4d31077985..6421396e66bb2e 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -46,12 +46,18 @@ export function createExecutionEnqueuerFunction({ ); } - const actionTypeId = await getActionTypeId( + const { actionTypeId, name, isMissingSecrets } = await getAction( unsecuredSavedObjectsClient, preconfiguredActions, id ); + if (isMissingSecrets) { + throw new Error( + `Unable to execute action because no secrets are defined for the "${name}" connector.` + ); + } + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } @@ -91,18 +97,16 @@ function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorO : {}; } -async function getActionTypeId( +async function getAction( unsecuredSavedObjectsClient: SavedObjectsClientContract, preconfiguredActions: PreConfiguredAction[], actionId: string -): Promise { +): Promise { const pcAction = preconfiguredActions.find((action) => action.id === actionId); if (pcAction) { - return pcAction.actionTypeId; + return pcAction; } - const { - attributes: { actionTypeId }, - } = await unsecuredSavedObjectsClient.get('action', actionId); - return actionTypeId; + const { attributes } = await unsecuredSavedObjectsClient.get('action', actionId); + return attributes; } diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1db990edef2a98..7bc965f0aa5c81 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -269,7 +269,7 @@ export class AlertsClient { throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } - this.validateActions(alertType, data.actions); + await this.validateActions(alertType, data.actions); const createTime = Date.now(); const { references, actions } = await this.denormalizeActions(data.actions); @@ -728,7 +728,7 @@ export class AlertsClient { data.params, alertType.validate?.params ); - this.validateActions(alertType, data.actions); + await this.validateActions(alertType, data.actions); const { actions, references } = await this.denormalizeActions(data.actions); const username = await this.getUserName(); @@ -1434,10 +1434,36 @@ export class AlertsClient { }; } - private validateActions( + private async validateActions( alertType: UntypedNormalizedAlertType, actions: NormalizedAlertAction[] - ): void { + ): Promise { + if (actions.length === 0) { + return; + } + + // check for actions using connectors with missing secrets + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(actions.map((action) => action.id))]; + const actionResults = (await actionsClient.getBulk(actionIds)) || []; + const actionsUsingConnectorsWithMissingSecrets = actionResults.filter( + (result) => result.isMissingSecrets + ); + + if (actionsUsingConnectorsWithMissingSecrets.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertsClient.validateActions.misconfiguredConnector', { + defaultMessage: 'Invalid connectors: {groups}', + values: { + groups: actionsUsingConnectorsWithMissingSecrets + .map((connector) => connector.name) + .join(', '), + }, + }) + ); + } + + // check for actions with invalid action groups const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map((action) => action.group); const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index 6f493ced473719..21974cff5eb2f7 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -93,9 +93,30 @@ function getMockData( describe('create()', () => { let alertsClient: AlertsClient; + let actionsClient: jest.Mocked; - beforeEach(() => { + beforeEach(async () => { alertsClient = new AlertsClient(alertsClientParams); + actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); }); describe('authorization', () => { @@ -104,19 +125,6 @@ describe('create()', () => { bar: boolean; }> ): Promise { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -460,7 +468,6 @@ describe('create()', () => { "scheduledTaskId": "task-123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); }); @@ -557,6 +564,39 @@ describe('create()', () => { }, ], }); + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -683,19 +723,6 @@ describe('create()', () => { test('creates a disabled alert', async () => { const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -761,19 +788,6 @@ describe('create()', () => { test('should trim alert name when creating API key', async () => { const data = getMockData({ name: ' my alert name ' }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1275,10 +1289,8 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); // Reset from default behaviour - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; actionsClient.getBulk.mockReset(); actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); @@ -1292,19 +1304,6 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -1329,19 +1328,6 @@ describe('create()', () => { test('attempts to remove saved object if scheduling failed', async () => { const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1386,19 +1372,6 @@ describe('create()', () => { test('returns task manager error if cleanup fails, logs to console', async () => { const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1455,19 +1428,6 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1579,19 +1539,6 @@ describe('create()', () => { test(`doesn't create API key for disabled alerts`, async () => { const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1722,4 +1669,32 @@ describe('create()', () => { `"Fail"` ); }); + + test('throws error when adding action using connector with missing secrets', async () => { + const data = getMockData(); + // Reset from default behaviour + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: true, + name: 'email connector', + isPreconfigured: false, + }, + ]); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid connectors: email connector"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index cdbfbbac9f9a13..c5e30e29efb445 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -60,6 +60,7 @@ setGlobalDate(); describe('update()', () => { let alertsClient: AlertsClient; + let actionsClient: jest.Mocked; const existingAlert = { id: '1', type: 'alert', @@ -96,8 +97,28 @@ describe('update()', () => { }, }; - beforeEach(() => { + beforeEach(async () => { alertsClient = new AlertsClient(alertsClientParams); + actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ @@ -113,6 +134,39 @@ describe('update()', () => { }); test('updates given parameters', async () => { + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -342,25 +396,11 @@ describe('update()', () => { "version": "123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); }); it('calls the createApiKey function', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, @@ -530,19 +570,6 @@ describe('update()', () => { enabled: false, }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -784,19 +811,6 @@ describe('update()', () => { }); it('should trim alert name in the API key name', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -842,19 +856,6 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -914,28 +915,39 @@ describe('update()', () => { it('swallows error when getDecryptedAsInternalUser throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, }, - { - id: '2', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test2', - }, - references: [], + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, }, - ], - }); + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1040,19 +1052,6 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '234', name: '234', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); await expect( alertsClient.update({ @@ -1101,19 +1100,6 @@ describe('update()', () => { async executor() {}, producer: 'alerts', }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: alertId, type: 'alert', @@ -1313,6 +1299,84 @@ describe('update()', () => { }); }); + test('throws error when updating action using connector with missing secrets', async () => { + // Reset from default behaviour + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'tes2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: true, + name: 'another connector', + isPreconfigured: false, + }, + ]); + + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid connectors: another connector"`); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + describe('authorization', () => { beforeEach(() => { unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts index cf424ea1e7317a..7011ec016c089f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts @@ -23,11 +23,13 @@ const transformConnector: RewriteRequestCase< connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, referenced_by_count: referencedByCount, + is_missing_secrets: isMissingSecrets, ...res }) => ({ actionTypeId, isPreconfigured, referencedByCount, + isMissingSecrets, ...res, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts index e6e74f3f3c059d..ba5c8a214b6ae7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts @@ -23,10 +23,16 @@ const rewriteBodyRequest: RewriteResponseCase< const rewriteBodyRes: RewriteRequestCase< ActionConnectorProps, Record> -> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ +> = ({ + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + is_missing_secrets: isMissingSecrets, + ...res +}) => ({ ...res, actionTypeId, isPreconfigured, + isMissingSecrets, }); export async function createActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts index 1bc0cefc2723b2..f2319ace29d685 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts @@ -15,10 +15,16 @@ import type { const rewriteBodyRes: RewriteRequestCase< ActionConnectorProps, Record> -> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ +> = ({ + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + is_missing_secrets: isMissingSecrets, + ...res +}) => ({ ...res, actionTypeId, isPreconfigured, + isMissingSecrets, }); export async function updateActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 26a101cedf9552..174407e7edec50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -124,6 +124,72 @@ describe('action_form', () => { actionParamsFields: mockedActionParamsFields, }; + const allActions = [ + { + secrets: {}, + isMissingSecrets: false, + id: 'test', + actionTypeId: actionType.id, + name: 'Test connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test2', + actionTypeId: actionType.id, + name: 'Test connector 2', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test3', + actionTypeId: preconfiguredOnly.id, + name: 'Preconfigured Only', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test4', + actionTypeId: preconfiguredOnly.id, + name: 'Regular connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'Non consumer connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: '.jira', + actionTypeId: disabledByActionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, + { + secrets: null, + isMissingSecrets: true, + id: '.jira', + actionTypeId: actionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, + ]; + const useKibanaMock = useKibana as jest.Mocked; describe('action_form in alert', () => { @@ -131,56 +197,7 @@ describe('action_form', () => { const actionTypeRegistry = actionTypeRegistryMock.create(); const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); - loadAllActions.mockResolvedValueOnce([ - { - secrets: {}, - id: 'test', - actionTypeId: actionType.id, - name: 'Test connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: 'test2', - actionTypeId: actionType.id, - name: 'Test connector 2', - config: {}, - isPreconfigured: true, - }, - { - secrets: {}, - id: 'test3', - actionTypeId: preconfiguredOnly.id, - name: 'Preconfigured Only', - config: {}, - isPreconfigured: true, - }, - { - secrets: {}, - id: 'test4', - actionTypeId: preconfiguredOnly.id, - name: 'Regular connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: '.servicenow', - actionTypeId: '.servicenow', - name: 'Non consumer connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: '.jira', - actionTypeId: disabledByActionType.id, - name: 'Connector with disabled action group', - config: {}, - isPreconfigured: false, - }, - ]); + loadAllActions.mockResolvedValueOnce(allActions); const mocks = coreMock.createSetup(); const [ { @@ -467,6 +484,14 @@ describe('action_form', () => { ); actionOption.first().simulate('click'); const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); + const numConnectors = allActions.filter((action) => action.actionTypeId === actionType.id) + .length; + const numConnectorsWithMissingSecrets = allActions.filter( + (action) => action.actionTypeId === actionType.id && action.isMissingSecrets + ).length; + expect((combobox.first().props() as any).options.length).toEqual( + numConnectors - numConnectorsWithMissingSecrets + ); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 02e96b5fd05c5c..55ebbbc6f3edd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -141,7 +141,7 @@ export const ActionForm = ({ try { setIsLoadingConnectors(true); const loadedConnectors = await loadConnectors({ http }); - setConnectors(loadedConnectors); + setConnectors(loadedConnectors.filter((connector) => !connector.isMissingSecrets)); } catch (e) { toasts.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1fd031cda6d961..6db5634be2221a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -141,6 +141,7 @@ export interface ActionConnectorProps { referencedByCount?: number; config: Config; isPreconfigured: boolean; + isMissingSecrets?: boolean; } export type PreConfiguredActionConnector = Omit<