[Actions] Set system actions on Kibana start (#160983)

## Summary

This PR:

- Adds the ability to create system action types
- Creates system connectors on Kibana `start` from the system action
types
- Prevents system action to be created/updated/deleted
- Return system actions from the get/getAll endpoints

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2023-07-07 20:39:29 +03:00 committed by GitHub
parent 552a3a6553
commit 67fc8333e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 3153 additions and 872 deletions

View file

@ -50,6 +50,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
enabledInLicense: true,
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['general'],
isSystemActionType: false,
id: '.gen-ai',
name: 'Generative AI',
enabled: true,

View file

@ -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',

View file

@ -22,6 +22,7 @@ export interface ActionType {
enabledInLicense: boolean;
minimumLicenseRequired: LicenseType;
supportedFeatureIds: string[];
isSystemActionType: boolean;
}
export enum InvalidEmailReason {

View file

@ -17,6 +17,7 @@ const createActionTypeRegistryMock = () => {
ensureActionTypeEnabled: jest.fn(),
isActionTypeEnabled: jest.fn(),
isActionExecutable: jest.fn(),
isSystemActionType: jest.fn(),
getUtils: jest.fn(),
};
return mocked;

View file

@ -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);
});
});
});

View file

@ -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<Config, Secrets, Params, ExecutorResultData>) {
// 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,
}));
}

View file

@ -27,6 +27,7 @@ const createActionsClientMock = () => {
listTypes: jest.fn(),
isActionTypeEnabled: jest.fn(),
isPreconfigured: jest.fn(),
isSystemAction: jest.fn(),
getGlobalExecutionKpiWithAuth: jest.fn(),
getGlobalExecutionLogWithAuth: jest.fn(),
};

View file

@ -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',
secrets: {},
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',

View file

@ -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<void>;
ephemeralExecutionEnqueuer: ExecutionEnqueuer<RunNowResult>;
@ -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<ActionResult> {
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<FindActionResult[]> {
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<ActionResult[]> {
try {
@ -490,17 +517,19 @@ export class ActionsClient {
}
const actionResults = new Array<ActionResult>();
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({

View file

@ -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();
});
});

View file

@ -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<void> {
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<void> {
return async function execute(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
@ -153,15 +153,16 @@ export function createBulkExecutionEnqueuerFunction({
const actionTypeIds: Record<string, string> = {};
const spaceIds: Record<string, string> = {};
const connectorIsPreconfigured: Record<string, boolean> = {};
const connectorIsInMemory: Record<string, boolean> = {};
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<RunNowResult> {
return async function execute(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
{ id, params, spaceId, source, consumer, apiKey, executionId }: ExecuteOptions
): Promise<RunNowResult> {
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<RawAction>('action', actionId);
return { action: attributes, isPreconfigured: false };
return { action: attributes, isInMemory: false };
}
async function getConnectors(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
preconfiguredConnectors: PreConfiguredAction[],
inMemoryConnectors: InMemoryConnector[],
connectorIds: string[]
): Promise<GetConnectorsResult[]> {
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,
});

View file

@ -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,
},
]);
});
});

View file

@ -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;
};

View file

@ -21,255 +21,217 @@ 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: {},
},
],
});
internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
id: '234',
type: 'action_task_params',
attributes: {
actionId: '123',
},
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: '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 {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
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: [
{
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 {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([
{
type: 'action_task_params',
attributes: {
actionId: '123',
params: { baz: false },
apiKey: null,
source: 'SAVED_OBJECT',
},
references: [
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: sourceUuid,
name: 'source',
type: 'alert',
id: '123',
actionTypeId: '.email',
config: {},
isPreconfigured,
isDeprecated: false,
isSystemAction,
name: 'x',
secrets: {},
},
],
},
{
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: [
internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
id: '234',
type: 'action_task_params',
attributes: {
actionId: '123',
},
references: [],
},
{
id: '345',
type: 'action_task_params',
attributes: {
actionId: '123',
},
references: [],
},
],
});
await executeFn(internalSavedObjectsRepository, [
{
id: '123',
actionTypeId: '.email',
config: {},
isPreconfigured: true,
isDeprecated: false,
isSystemAction: false,
name: 'x',
secrets: {},
params: { baz: false },
source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }),
},
],
});
internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
id: '234',
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 {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
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.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 {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([
{
type: 'action_task_params',
attributes: {
actionId: '123',
params: { baz: false },
apiKey: null,
source: 'SAVED_OBJECT',
},
references: [
{
@ -280,10 +242,156 @@ describe('bulkExecute()', () => {
],
},
{
id: '345',
type: 'action_task_params',
attributes: {
actionId: '123',
params: { baz: true },
apiKey: null,
source: 'NOTIFICATION',
},
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',
namespace: 'some-namespace',
type: 'some-type',
},
],
},
]);
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:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
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',
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
},
],
},
references: [
{
@ -293,215 +401,143 @@ describe('bulkExecute()', () => {
},
],
},
],
});
await executeFn(internalSavedObjectsRepository, [
{
id: '123',
params: { baz: false },
source: asSavedObjectExecutionSource(source),
},
{
id: '123',
params: { baz: true },
source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }),
relatedSavedObjects: [
]);
}
);
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',
namespace: 'some-namespace',
type: 'some-type',
id: '123',
actionTypeId: '.email',
config: {},
isPreconfigured,
isDeprecated: false,
isSystemAction,
name: 'x',
secrets: {},
},
],
},
]);
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:.email",
},
Object {
"params": Object {
"actionTaskParamsId": "345",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:.email",
},
],
]
`);
expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([
{
type: 'action_task_params',
attributes: {
actionId: '123',
params: { baz: false },
apiKey: null,
source: 'SAVED_OBJECT',
},
references: [
});
await expect(
executeFn(internalSavedObjectsRepository, [
{
id: sourceUuid,
name: 'source',
type: 'alert',
id: '123',
params: { baz: false },
source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }),
},
],
},
{
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',
},
],
},
references: [
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
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.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');
});
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: '123',
params: { baz: true },
source: asNotificationExecutionSource({ connectorId: 'abc', requesterId: 'foo' }),
},
])
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
}
);
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');
});
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"`
);
}
);
});

View file

@ -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<T> = (
export function createBulkUnsecuredExecutionEnqueuerFunction({
taskManager,
connectorTypeRegistry,
preconfiguredConnectors,
inMemoryConnectors,
}: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer<void> {
return async function execute(
internalSavedObjectsRepository: ISavedObjectsRepository,
@ -51,24 +51,23 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({
const connectorTypeIds: Record<string, string> = {};
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;

View file

@ -22,7 +22,7 @@ export type {
ActionResult,
ActionTypeExecutorOptions,
ActionType,
PreConfiguredAction,
InMemoryConnector,
ActionsApiRequestHandlerContext,
FindActionResult,
} from './types';

View file

@ -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<ActionType> = {
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<ActionType> = {
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<ActionType> = {
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<ActionType> = {
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({

View file

@ -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<Source>;
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<ActionInfo> {
// 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,
};
}

View file

@ -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',

View file

@ -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,

View file

@ -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: {

View file

@ -35,7 +35,7 @@ interface CreateActionEventLogRecordParams {
relation?: string;
}>;
relatedSavedObjects?: RelatedSavedObjects;
isPreconfigured?: boolean;
isInMemory?: boolean;
source?: ActionExecutionSource<unknown>;
}
@ -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] } : {}),

View file

@ -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,

View file

@ -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,

View file

@ -6,10 +6,10 @@
*/
import { isPlainObject } from 'lodash';
import { PreConfiguredAction, RawAction } from '../types';
import { InMemoryConnector, RawAction } from '../types';
export type ConnectorWithOptionalDeprecation = Omit<PreConfiguredAction, 'isDeprecated'> &
Pick<Partial<PreConfiguredAction>, 'isDeprecated'>;
export type ConnectorWithOptionalDeprecation = Omit<InMemoryConnector, 'isDeprecated'> &
Pick<Partial<InMemoryConnector>, 'isDeprecated'>;
const isObject = (obj: unknown): obj is Record<string, unknown> => isPlainObject(obj);

View file

@ -86,7 +86,7 @@ const actionExecutorInitializerParams = {
getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()),
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
eventLogger,
preconfiguredActions: [],
inMemoryConnectors: [],
};
const taskRunnerFactoryInitializerParams = {
spaceIdToNamespace,

View file

@ -45,7 +45,7 @@ const createStartMock = () => {
getActionsAuthorizationWithRequest: jest
.fn()
.mockReturnValue(actionsAuthorizationMock.create()),
preconfiguredActions: [],
inMemoryConnectors: [],
renderActionParameterTemplates: jest.fn(),
};
return mock;

View file

@ -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<ActionsConfig>(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<ActionsConfig>(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<ActionsConfig>(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,
});
const pluginStart = await plugin.start(coreStart, pluginsStart);
expect(pluginStart.preconfiguredActions.length).toEqual(1);
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
});
it('should handle preconfiguredAlertHistoryEsIndex = true', async () => {
setup(getConfig({ preconfiguredAlertHistoryEsIndex: 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,
});
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: {},
},
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({}) },
},
})
);
// 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);
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);
expect(pluginStart.inMemoryConnectors.length).toEqual(1);
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(
true
);
});
it('should handle preconfiguredAlertHistoryEsIndex = true', async () => {
setup(getConfig({ preconfiguredAlertHistoryEsIndex: 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,
});
const pluginStart = await plugin.start(coreStart, pluginsStart);
expect(pluginStart.inMemoryConnectors.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.`
);
});
});
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({}) },
},
isSystemActionType: true,
executor,
});
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);
});
});
});

View file

@ -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<ActionsAuthorization>;
preconfiguredActions: PreConfiguredAction[];
inMemoryConnectors: InMemoryConnector[];
getUnsecuredActionsClient(): IUnsecuredActionsClient;
@ -200,7 +201,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
private isESOCanEncrypt?: boolean;
private usageCounter?: UsageCounter;
private readonly telemetryLogger: Logger;
private readonly preconfiguredActions: PreConfiguredAction[];
private inMemoryConnectors: InMemoryConnector[];
private inMemoryMetrics: InMemoryMetrics;
constructor(initContext: PluginInitializerContext) {
@ -210,7 +211,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
resolveCustomHosts(this.logger, initContext.config.get<ActionsConfig>())
);
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<PluginSetupContract, PluginStartCon
const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig);
if (this.actionsConfig.preconfiguredAlertHistoryEsIndex) {
this.preconfiguredActions.push(getAlertHistoryEsIndex());
this.inMemoryConnectors.push(getAlertHistoryEsIndex());
}
for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) {
@ -255,7 +256,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
isPreconfigured: true,
isSystemAction: false,
};
this.preconfiguredActions.push({
this.inMemoryConnectors.push({
...rawPreconfiguredConnector,
isDeprecated: isConnectorDeprecated(rawPreconfiguredConnector),
});
@ -272,7 +273,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
taskManager: plugins.taskManager,
actionsConfigUtils,
licenseState: this.licenseState,
preconfiguredActions: this.preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
});
this.taskRunnerFactory = taskRunnerFactory;
this.actionTypeRegistry = actionTypeRegistry;
@ -284,7 +285,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
plugins.encryptedSavedObjects,
this.actionTypeRegistry!,
plugins.taskManager.index,
this.preconfiguredActions
this.inMemoryConnectors
);
const usageCollection = plugins.usageCollection;
@ -307,7 +308,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
this.telemetryLogger,
plugins.taskManager,
core,
this.preconfiguredActions,
this.getInMemoryConnectors,
eventLogIndex
);
}
@ -361,8 +362,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
subActionFramework.registerConnector(connector);
},
isPreconfiguredConnector: (connectorId: string): boolean => {
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<PluginSetupContract, PluginStartCon
actionTypeRegistry,
taskRunnerFactory,
isESOCanEncrypt,
preconfiguredActions,
instantiateAuthorization,
getUnsecuredSavedObjectsClient,
} = this;
@ -395,6 +396,16 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
includedHiddenTypes,
});
/**
* Warning: this call mutates the inMemory collection
*
* Warning: it maybe possible for the task manager to start before
* the system actions are being set.
*
* Issue: https://github.com/elastic/kibana/issues/160797
*/
this.setSystemActions();
const getActionsClientWithRequest = async (
request: KibanaRequest,
authorizationContext?: ActionExecutionSource<unknown>
@ -416,7 +427,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
actionTypeRegistry: actionTypeRegistry!,
kibanaIndices: core.savedObjects.getAllIndices(),
scopedClusterClient: core.elasticsearch.client.asScoped(request),
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
request,
authorization: instantiateAuthorization(
request,
@ -427,19 +438,19 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
taskManager: plugins.taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
}),
executionEnqueuer: createExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
}),
bulkExecutionEnqueuer: createBulkExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
}),
auditLogger: this.security?.audit.asScoped(request),
usageCounter: this.usageCounter,
@ -464,7 +475,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
executionEnqueuer: createBulkUnsecuredExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
connectorTypeRegistry: actionTypeRegistry!,
preconfiguredConnectors: preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
}),
});
};
@ -501,7 +512,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
),
encryptedSavedObjectsClient,
actionTypeRegistry: actionTypeRegistry!,
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
});
taskRunnerFactory!.initialize({
@ -543,7 +554,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
},
getActionsClientWithRequest: secureGetActionsClientWithRequest,
getUnsecuredActionsClient,
preconfiguredActions,
inMemoryConnectors: this.inMemoryConnectors,
renderActionParameterTemplates: (...args) =>
renderActionParameterTemplates(actionTypeRegistry, ...args),
};
@ -589,13 +600,20 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
};
}
private getInMemoryConnectors = () => this.inMemoryConnectors;
private setSystemActions = () => {
const systemConnectors = createSystemConnectors(this.actionTypeRegistry?.list() ?? []);
this.inMemoryConnectors = [...this.inMemoryConnectors, ...systemConnectors];
};
private createRouteHandlerContext = (
core: CoreSetup<ActionsPluginsStart>
): IContextProvider<ActionsRequestHandlerContext, 'actions'> => {
const {
actionTypeRegistry,
isESOCanEncrypt,
preconfiguredActions,
getInMemoryConnectors,
actionExecutor,
instantiateAuthorization,
security,
@ -606,7 +624,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
return async function actionsRouteHandlerContext(context, request) {
const [{ savedObjects }, { taskManager, encryptedSavedObjects, eventLog }] =
await core.getStartServices();
const coreContext = await context.core;
const inMemoryConnectors = getInMemoryConnectors();
return {
getActionsClient: () => {
@ -625,7 +645,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
actionTypeRegistry: actionTypeRegistry!,
kibanaIndices: savedObjects.getAllIndices(),
scopedClusterClient: coreContext.elasticsearch.client,
preconfiguredActions,
inMemoryConnectors,
request,
authorization: instantiateAuthorization(request),
actionExecutor: actionExecutor!,
@ -633,19 +653,19 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors,
}),
executionEnqueuer: createExecutionEnqueuerFunction({
taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors,
}),
bulkExecutionEnqueuer: createBulkExecutionEnqueuerFunction({
taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
inMemoryConnectors,
}),
auditLogger: security?.audit.asScoped(request),
usageCounter,

View file

@ -6,11 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
import { PreConfiguredAction } from '../../types';
import { InMemoryConnector } from '../../types';
import { AlertHistoryEsIndexConnectorId, AlertHistoryDefaultIndexName } from '../../../common';
const EsIndexActionTypeId = '.index';
export function getAlertHistoryEsIndex(): Readonly<PreConfiguredAction> {
export function getAlertHistoryEsIndex(): Readonly<InMemoryConnector> {
return Object.freeze({
name: i18n.translate('xpack.actions.alertHistoryEsIndexConnector.name', {
defaultMessage: 'Alert history Elasticsearch index',

View file

@ -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,
},
];

View file

@ -23,6 +23,7 @@ const rewriteBodyRes: RewriteResponseCase<ActionType[]> = (results) => {
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
isSystemActionType,
...res
}) => ({
...res,
@ -30,6 +31,7 @@ const rewriteBodyRes: RewriteResponseCase<ActionType[]> = (results) => {
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
is_system_action_type: isSystemActionType,
})
);
};

View file

@ -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,
},
];

View file

@ -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);
});
});

View file

@ -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<ActionTaskParams> => true,
pipeMigrations(getUseSavedObjectReferencesFn(preconfiguredActions))
pipeMigrations(getUseSavedObjectReferencesFn(inMemoryConnectors))
);
const migrationActionsTaskParams800 = createEsoMigration(
@ -86,22 +86,22 @@ function executeMigrationWithErrorHandling(
};
}
export function isPreconfiguredAction(
export function isInMemoryAction(
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>,
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<ActionTaskParams>) => {
return useSavedObjectReferences(doc, preconfiguredActions);
return useSavedObjectReferences(doc, inMemoryConnectors);
};
}
function useSavedObjectReferences(
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>,
preconfiguredActions: PreConfiguredAction[]
inMemoryConnectors: InMemoryConnector[]
): SavedObjectUnsanitizedDoc<ActionTaskParams> {
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',

View file

@ -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,

View file

@ -84,7 +84,7 @@ export interface ActionResult<Config extends ActionTypeConfig = ActionTypeConfig
isSystemAction: boolean;
}
export interface PreConfiguredAction<
export interface InMemoryConnector<
Config extends ActionTypeConfig = ActionTypeConfig,
Secrets extends ActionTypeSecrets = ActionTypeSecrets
> extends ActionResult<Config> {
@ -140,7 +140,7 @@ export interface ActionType<
secrets: ValidatorType<Secrets>;
connector?: (config: Config, secrets: Secrets) => string | null;
};
isSystemAction?: boolean;
isSystemActionType?: boolean;
renderParameterTemplates?: RenderParameterTemplates<Params>;
executor: ExecutorType<Config, Secrets, Params, ExecutorResultData>;
}

View file

@ -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 =

View file

@ -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 =

View file

@ -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(

View file

@ -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,
},
];

View file

@ -35,6 +35,7 @@ describe('client', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
isSystemActionType: false,
},
{
id: '.servicenow',
@ -44,6 +45,7 @@ describe('client', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
isSystemActionType: false,
},
{
id: '.unsupported',
@ -53,6 +55,7 @@ describe('client', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting'],
isSystemActionType: false,
},
{
id: '.swimlane',
@ -62,6 +65,7 @@ describe('client', () => {
enabledInLicense: false,
minimumLicenseRequired: 'basic' as const,
supportedFeatureIds: ['alerting', 'cases'],
isSystemActionType: false,
},
];

View file

@ -85,6 +85,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
is_system_action_type: isSystemActionType,
...res
}: AsApiContract<ActionType>) => ({
...res,
@ -92,6 +93,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
isSystemActionType,
})
);
};

View file

@ -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,
},
];

View file

@ -19,12 +19,14 @@ const rewriteBodyReq: RewriteRequestCase<ActionType> = ({
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
is_system_action_type: isSystemActionType,
...res
}: AsApiContract<ActionType>) => ({
enabledInConfig,
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
isSystemActionType,
...res,
});

View file

@ -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);

View file

@ -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(`

View file

@ -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,
},
};

View file

@ -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(
<ConnectorAddModal
@ -139,6 +141,7 @@ describe('connector_add_modal', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
isSystemActionType: false,
};
const wrapper = mountWithIntl(
<ConnectorAddModal

View file

@ -57,6 +57,7 @@ describe('connectors_selection', () => {
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
isSystemActionType: false,
},
};

View file

@ -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);

View file

@ -24,6 +24,7 @@ describe('settings', () => {
minimumLicenseRequired: 'gold',
name: 'Slack',
supportedFeatureIds: ['uptime'],
isSystemActionType: false,
},
]);
});

View file

@ -172,6 +172,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
enabled_in_license: enabledInLicense,
minimum_license_required: minimumLicenseRequired,
supported_feature_ids: supportedFeatureIds,
is_system_action_type: isSystemActionType,
...res
}: AsApiContract<ActionType>) => ({
...res,
@ -179,6 +180,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
enabledInLicense,
minimumLicenseRequired,
supportedFeatureIds,
isSystemActionType,
})
);
};

View file

@ -64,6 +64,7 @@ const enabledActionTypes = [
'test.throw',
'test.excluded',
'test.capped',
'test.system-action',
];
export function createTestConfig(name: string, options: CreateTestConfigOptions) {

View file

@ -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;
}

View file

@ -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)}`);
}
});
});
}
});

View file

@ -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)}`);
}
});
});
}
});

View file

@ -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)}`);
}
});
});
}
});

View file

@ -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,

View file

@ -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)}`);
}
});
});
}
});

View file

@ -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

View file

@ -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.',
});
});
});
});
}

View file

@ -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,
});
});
});
});
}

View file

@ -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,

View file

@ -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`
);
});
});

View file

@ -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.',
});
});