Skip to content

Commit

Permalink
[Actions] Disable 'Resolved' action group for ServiceNow, Jira and IB…
Browse files Browse the repository at this point in the history
…M Resilient action types (#83829) (#85420)

* Adding disabled action groups to action type definition

* Adding tests

* Adding tests

* renamed Resolved to Recovered

* fixed missing import

* fixed buggy default message behaviour

* added missing test

* fixed typing

* fixed resolved in tests

* allows alert types to specify their own custom recovery group name

* removed unnecesery field on always fires

* allows alert types to specify their own custom recovery group

* fixed mock alert types throughout unit tests

* fixed typing issues

* reduce repetition of mock data

* fixed alert type list test

* support legacy event log alert recovery syntax

* added doc

* removed unneeded change in jira

* correct callback name in siem

* renamed resolved to  recovered

* fixed mistaken rename

* Moving to alert plugin

* Updating tests

* elvated default params to alert concern instead of actions concern

* made default params optional

* Adding test

* Moving where default action params are retrieved

* Revert "Moving where default action params are retrieved"

This reverts commit 76e7608.

* Moving where default action params are retrieved

* Cleanup

* Fixing test

* PR fixes

Co-authored-by: Gidi Meir Morris <github@gidi.io>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Gidi Meir Morris <github@gidi.io>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people committed Dec 9, 2020
1 parent 9b552e3 commit 8236672
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 4 deletions.
19 changes: 19 additions & 0 deletions x-pack/plugins/alerts/common/disabled_action_groups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isActionGroupDisabledForActionTypeId } from './disabled_action_groups';
import { RecoveredActionGroup } from './builtin_action_groups';

test('returns false if action group id has no disabled types', () => {
expect(isActionGroupDisabledForActionTypeId('enabledActionGroup', '.jira')).toBeFalsy();
});

test('returns false if action group id does not contains type', () => {
expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.email')).toBeFalsy();
});

test('returns true if action group id does contain type', () => {
expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.jira')).toBeTruthy();
});
24 changes: 24 additions & 0 deletions x-pack/plugins/alerts/common/disabled_action_groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RecoveredActionGroup } from './builtin_action_groups';

const DisabledActionGroupsByActionType: Record<string, string[]> = {
[RecoveredActionGroup.id]: ['.jira', '.servicenow', '.resilient'],
};

export const DisabledActionTypeIdsForActionGroup: Map<string, string[]> = new Map(
Object.entries(DisabledActionGroupsByActionType)
);

export function isActionGroupDisabledForActionTypeId(
actionGroup: string,
actionTypeId: string
): boolean {
return (
DisabledActionTypeIdsForActionGroup.has(actionGroup) &&
DisabledActionTypeIdsForActionGroup.get(actionGroup)!.includes(actionTypeId)
);
}
1 change: 1 addition & 0 deletions x-pack/plugins/alerts/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './alert_task_instance';
export * from './alert_navigation';
export * from './alert_instance_summary';
export * from './builtin_action_groups';
export * from './disabled_action_groups';

export interface AlertingFrameworkHealth {
isSufficientlySecure: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
import ActionForm from './action_form';
import { useKibana } from '../../../common/lib/kibana';
import {
RecoveredActionGroup,
isActionGroupDisabledForActionTypeId,
} from '../../../../../alerts/common';

jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
Expand Down Expand Up @@ -65,6 +70,21 @@ describe('action_form', () => {
actionParamsFields: mockedActionParamsFields,
};

const disabledByActionType = {
id: '.jira',
iconClass: 'test',
selectMessage: 'test',
validateConnector: (): ValidationResult => {
return { errors: {} };
},
validateParams: (): ValidationResult => {
const validationResult = { errors: {} };
return validationResult;
},
actionConnectorFields: null,
actionParamsFields: mockedActionParamsFields,
};

const disabledByLicenseActionType = {
id: 'disabled-by-license',
iconClass: 'test',
Expand Down Expand Up @@ -112,7 +132,7 @@ describe('action_form', () => {
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;

describe('action_form in alert', () => {
async function setup(customActions?: AlertAction[]) {
async function setup(customActions?: AlertAction[], customRecoveredActionGroup?: string) {
const actionTypeRegistry = actionTypeRegistryMock.create();

const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
Expand Down Expand Up @@ -159,6 +179,14 @@ describe('action_form', () => {
},
isPreconfigured: false,
},
{
secrets: {},
id: '.jira',
actionTypeId: disabledByActionType.id,
name: 'Connector with disabled action group',
config: {},
isPreconfigured: false,
},
]);
const mocks = coreMock.createSetup();
const [
Expand All @@ -179,6 +207,7 @@ describe('action_form', () => {
actionType,
disabledByConfigActionType,
disabledByLicenseActionType,
disabledByActionType,
preconfiguredOnly,
actionTypeWithoutParams,
]);
Expand Down Expand Up @@ -223,12 +252,24 @@ describe('action_form', () => {
context: [{ name: 'contextVar', description: 'context var1' }],
}}
defaultActionGroupId={'default'}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => {
const recoveryActionGroupId = customRecoveredActionGroup
? customRecoveredActionGroup
: 'recovered';
return isActionGroupDisabledForActionTypeId(
actionGroupId === recoveryActionGroupId ? RecoveredActionGroup.id : actionGroupId,
actionTypeId
);
}}
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
actionGroups={[
{ id: 'default', name: 'Default', defaultActionMessage },
{ id: 'recovered', name: 'Recovered' },
{
id: customRecoveredActionGroup ? customRecoveredActionGroup : 'recovered',
name: customRecoveredActionGroup ? 'I feel better' : 'Recovered',
},
]}
setActionGroupIdByIndex={(group: string, index: number) => {
initialAlert.actions[index].group = group;
Expand Down Expand Up @@ -280,6 +321,14 @@ describe('action_form', () => {
enabledInLicense: false,
minimumLicenseRequired: 'gold',
},
{
id: '.jira',
name: 'Disabled by action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
{
id: actionTypeWithoutParams.id,
name: 'Action type without params',
Expand Down Expand Up @@ -342,18 +391,91 @@ describe('action_form', () => {
Array [
Object {
"data-test-subj": "addNewActionConnectorActionGroup-0-option-default",
"disabled": false,
"inputDisplay": "Default",
"value": "default",
},
Object {
"data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered",
"disabled": false,
"inputDisplay": "Recovered",
"value": "recovered",
},
]
`);
});

it('renders disabled action groups for selected action type', async () => {
const wrapper = await setup([
{
group: 'recovered',
id: 'test',
actionTypeId: disabledByActionType.id,
params: {
message: '',
},
},
]);
const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
`[data-test-subj="addNewActionConnectorActionGroup-1"]`
);
expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "addNewActionConnectorActionGroup-1-option-default",
"disabled": false,
"inputDisplay": "Default",
"value": "default",
},
Object {
"data-test-subj": "addNewActionConnectorActionGroup-1-option-recovered",
"disabled": true,
"inputDisplay": "Recovered (Not Currently Supported)",
"value": "recovered",
},
]
`);
});

it('renders disabled action groups for custom recovered action groups', async () => {
const wrapper = await setup(
[
{
group: 'iHaveRecovered',
id: 'test',
actionTypeId: disabledByActionType.id,
params: {
message: '',
},
},
],
'iHaveRecovered'
);
const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
`[data-test-subj="addNewActionConnectorActionGroup-1"]`
);
expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "addNewActionConnectorActionGroup-1-option-default",
"disabled": false,
"inputDisplay": "Default",
"value": "default",
},
Object {
"data-test-subj": "addNewActionConnectorActionGroup-1-option-iHaveRecovered",
"disabled": true,
"inputDisplay": "I feel better (Not Currently Supported)",
"value": "iHaveRecovered",
},
]
`);
});

it('renders available connectors for the selected action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface ActionAccordionFormProps {
setHasActionsWithBrokenConnector?: (value: boolean) => void;
actionTypeRegistry: ActionTypeRegistryContract;
getDefaultActionParams?: DefaultActionParamsGetter;
isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean;
}

interface ActiveActionConnectorState {
Expand All @@ -81,6 +82,7 @@ export const ActionForm = ({
setHasActionsWithBrokenConnector,
actionTypeRegistry,
getDefaultActionParams,
isActionGroupDisabledForActionType,
}: ActionAccordionFormProps) => {
const {
http,
Expand Down Expand Up @@ -345,6 +347,7 @@ export const ActionForm = ({
actionGroups={actionGroups}
defaultActionMessage={defaultActionMessage}
defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)}
isActionGroupDisabledForActionType={isActionGroupDisabledForActionType}
setActionGroupIdByIndex={setActionGroupIdByIndex}
onAddConnector={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type ActionTypeFormProps = {
connectors: ActionConnector[];
actionTypeRegistry: ActionTypeRegistryContract;
defaultParams: DefaultActionParams;
isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean;
} & Pick<
ActionAccordionFormProps,
| 'defaultActionGroupId'
Expand Down Expand Up @@ -94,6 +95,7 @@ export const ActionTypeForm = ({
actionGroups,
setActionGroupIdByIndex,
actionTypeRegistry,
isActionGroupDisabledForActionType,
defaultParams,
}: ActionTypeFormProps) => {
const {
Expand Down Expand Up @@ -145,6 +147,28 @@ export const ActionTypeForm = ({

const actionType = actionTypesIndex[actionItem.actionTypeId];

const actionGroupDisplay = (
actionGroupId: string,
actionGroupName: string,
actionTypeId: string
): string =>
isActionGroupDisabledForActionType
? isActionGroupDisabledForActionType(actionGroupId, actionTypeId)
? i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display',
{
defaultMessage: '{actionGroupName} (Not Currently Supported)',
values: { actionGroupName },
}
)
: actionGroupName
: actionGroupName;

const isActionGroupDisabled = (actionGroupId: string, actionTypeId: string): boolean =>
isActionGroupDisabledForActionType
? isActionGroupDisabledForActionType(actionGroupId, actionTypeId)
: false;

const optionsList = connectors
.filter(
(connectorItem) =>
Expand Down Expand Up @@ -191,7 +215,8 @@ export const ActionTypeForm = ({
data-test-subj={`addNewActionConnectorActionGroup-${index}`}
options={actionGroups.map(({ id: value, name }) => ({
value,
inputDisplay: name,
inputDisplay: actionGroupDisplay(value, name, actionItem.actionTypeId),
disabled: isActionGroupDisabled(value, actionItem.actionTypeId),
'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`,
}))}
valueOfSelected={selectedActionGroup.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ import {
} from '../../../types';
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { ActionForm } from '../action_connector_form';
import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import {
AlertActionParam,
ALERTS_FEATURE_ID,
RecoveredActionGroup,
isActionGroupDisabledForActionTypeId,
} from '../../../../../alerts/common';
import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities';
import { SolutionFilter } from './solution_filter';
import './alert_form.scss';
Expand Down Expand Up @@ -192,6 +197,7 @@ export const AlertForm = ({
setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId);
}
setAlertTypesIndex(index);

const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult);
setAvailableAlertTypes(availableAlertTypesResult);

Expand Down Expand Up @@ -331,6 +337,18 @@ export const AlertForm = ({

const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];

const isActionGroupDisabledForActionType = useCallback(
(alertType: AlertType, actionGroupId: string, actionTypeId: string): boolean => {
return isActionGroupDisabledForActionTypeId(
actionGroupId === alertType?.recoveryActionGroup?.id
? RecoveredActionGroup.id
: actionGroupId,
actionTypeId
);
},
[]
);

const AlertParamsExpressionComponent = alertTypeModel
? alertTypeModel.alertParamsExpression
: null;
Expand Down Expand Up @@ -513,6 +531,9 @@ export const AlertForm = ({
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
messageVariables={selectedAlertType.actionVariables}
defaultActionGroupId={defaultActionGroupId}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) =>
isActionGroupDisabledForActionType(selectedAlertType, actionGroupId, actionTypeId)
}
actionGroups={selectedAlertType.actionGroups.map((actionGroup) =>
actionGroup.id === selectedAlertType.recoveryActionGroup.id
? {
Expand Down

0 comments on commit 8236672

Please sign in to comment.