mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Actions] System actions authorization (#161341)
## Summary This PR adds the ability for system actions to be able to define their own Kibana privileges that need to be authorized before execution. Depends on: https://github.com/elastic/kibana/pull/160983 ### 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:
parent
c1fc644fbf
commit
2943fc9e06
45 changed files with 1631 additions and 127 deletions
|
@ -19,6 +19,7 @@ const createActionTypeRegistryMock = () => {
|
|||
isActionExecutable: jest.fn(),
|
||||
isSystemActionType: jest.fn(),
|
||||
getUtils: jest.fn(),
|
||||
getSystemActionKibanaPrivileges: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -248,6 +248,29 @@ describe('actionTypeRegistry', () => {
|
|||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('throws if the kibana privileges are defined but the action type is not a system action type', () => {
|
||||
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
|
||||
expect(() =>
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: jest.fn(),
|
||||
isSystemActionType: false,
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
executor,
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Kibana privilege authorization is only supported for system action types"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
@ -691,4 +714,92 @@ describe('actionTypeRegistry', () => {
|
|||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSystemActionKibanaPrivileges()', () => {
|
||||
it('should get the kibana privileges correctly for system actions', () => {
|
||||
const registry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
|
||||
registry.register({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: () => ['test/create'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
const result = registry.getSystemActionKibanaPrivileges('.cases');
|
||||
expect(result).toEqual(['test/create']);
|
||||
});
|
||||
|
||||
it('should return an empty array if the system action does not define any kibana privileges', () => {
|
||||
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.getSystemActionKibanaPrivileges('.cases');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if the action type is not a system action', () => {
|
||||
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 result = registry.getSystemActionKibanaPrivileges('foo');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should pass the params correctly', () => {
|
||||
const registry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']);
|
||||
|
||||
registry.register({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges,
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
registry.getSystemActionKibanaPrivileges('.cases', { foo: 'bar' });
|
||||
expect(getKibanaPrivileges).toHaveBeenCalledWith({ params: { foo: 'bar' } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -101,6 +101,22 @@ export class ActionTypeRegistry {
|
|||
public isSystemActionType = (actionTypeId: string): boolean =>
|
||||
Boolean(this.actionTypes.get(actionTypeId)?.isSystemActionType);
|
||||
|
||||
/**
|
||||
* Returns the kibana privileges of a system action type
|
||||
*/
|
||||
public getSystemActionKibanaPrivileges<Params extends ActionTypeParams = ActionTypeParams>(
|
||||
actionTypeId: string,
|
||||
params?: Params
|
||||
): string[] {
|
||||
const actionType = this.actionTypes.get(actionTypeId);
|
||||
|
||||
if (!actionType?.isSystemActionType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return actionType?.getKibanaPrivileges?.({ params }) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an action type to the action type registry
|
||||
*/
|
||||
|
@ -148,6 +164,15 @@ export class ActionTypeRegistry {
|
|||
);
|
||||
}
|
||||
|
||||
if (!actionType.isSystemActionType && actionType.getKibanaPrivileges) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.actions.actionTypeRegistry.register.invalidKibanaPrivileges', {
|
||||
defaultMessage:
|
||||
'Kibana privilege authorization is only supported for system action types',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const maxAttempts = this.actionsConfigUtils.getMaxAttempts({
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeMaxAttempts: actionType.maxAttempts,
|
||||
|
|
|
@ -198,7 +198,10 @@ describe('create()', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'create',
|
||||
actionTypeId: 'my-action-type',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create this type of action', async () => {
|
||||
|
@ -242,7 +245,10 @@ describe('create()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to create a "my-action-type" action]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'create',
|
||||
actionTypeId: 'my-action-type',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -847,7 +853,7 @@ describe('get()', () => {
|
|||
|
||||
await actionsClient.get({ id: '1' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('ensures user is authorised to get preconfigured type of action', async () => {
|
||||
|
@ -885,7 +891,7 @@ describe('get()', () => {
|
|||
|
||||
await actionsClient.get({ id: 'testPreconfigured' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('ensures user is authorised to get a system action', async () => {
|
||||
|
@ -919,7 +925,7 @@ describe('get()', () => {
|
|||
|
||||
await actionsClient.get({ id: 'system-connector-.cases' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to get the type of action', async () => {
|
||||
|
@ -943,7 +949,7 @@ describe('get()', () => {
|
|||
`[Error: Unauthorized to get a "my-action-type" action]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to get preconfigured of action', async () => {
|
||||
|
@ -987,7 +993,7 @@ describe('get()', () => {
|
|||
`[Error: Unauthorized to get a "my-action-type" action]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to get a system action', async () => {
|
||||
|
@ -1029,7 +1035,7 @@ describe('get()', () => {
|
|||
`[Error: Unauthorized to get a "system-connector-.cases" action]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1270,7 +1276,7 @@ describe('getAll()', () => {
|
|||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getAllOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -1282,7 +1288,7 @@ describe('getAll()', () => {
|
|||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1521,7 +1527,7 @@ describe('getBulk()', () => {
|
|||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getBulkOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -1533,7 +1539,7 @@ describe('getBulk()', () => {
|
|||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1762,7 +1768,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -1786,7 +1792,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to update actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1809,7 +1815,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Token URL must use http or https]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
|
||||
test('throws when tokenUrl does not contain hostname', async () => {
|
||||
|
@ -1831,7 +1837,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Token URL must contain hostname]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
|
||||
test('throws when tokenUrl is not in allowed hosts', async () => {
|
||||
|
@ -1857,7 +1863,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: URI not allowed]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
expect(configurationUtilities.ensureUriAllowed).toHaveBeenCalledWith(
|
||||
`https://testurl.service-now.com/oauth_token.do`
|
||||
);
|
||||
|
@ -2003,7 +2009,7 @@ describe('delete()', () => {
|
|||
describe('authorization', () => {
|
||||
test('ensures user is authorised to delete actions', async () => {
|
||||
await actionsClient.delete({ id: '1' });
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'delete' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -2015,7 +2021,7 @@ describe('delete()', () => {
|
|||
`[Error: Unauthorized to delete all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'delete' });
|
||||
});
|
||||
|
||||
test(`deletes any existing authorization tokens`, async () => {
|
||||
|
@ -2205,7 +2211,7 @@ describe('update()', () => {
|
|||
describe('authorization', () => {
|
||||
test('ensures user is authorised to update actions', async () => {
|
||||
await updateOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -2217,7 +2223,7 @@ describe('update()', () => {
|
|||
`[Error: Unauthorized to update all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'update' });
|
||||
});
|
||||
|
||||
test(`deletes any existing authorization tokens`, async () => {
|
||||
|
@ -2737,7 +2743,10 @@ describe('execute()', () => {
|
|||
},
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -2758,7 +2767,10 @@ describe('execute()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('tracks legacy RBAC', async () => {
|
||||
|
@ -2775,6 +2787,200 @@ describe('execute()', () => {
|
|||
});
|
||||
|
||||
expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith('execute', mockUsageCounter);
|
||||
expect(authorization.ensureAuthorized).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('ensures that system actions privileges are being authorized correctly', async () => {
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
auditLogger,
|
||||
usageCounter: mockUsageCounter,
|
||||
connectorTokenClient,
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
actionTypeRegistry.register({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: () => ['test/create'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'system-connector-.cases',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
});
|
||||
|
||||
test('does not authorize kibana privileges for non system actions', async () => {
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: 'my-action-type',
|
||||
secrets: {
|
||||
test: 'test1',
|
||||
},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
auditLogger,
|
||||
usageCounter: mockUsageCounter,
|
||||
connectorTokenClient,
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
actionTypeRegistry.register({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: () => ['test/create'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'testPreconfigured',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('pass the params to the actionTypeRegistry when authorizing system actions', async () => {
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
|
||||
const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
auditLogger,
|
||||
usageCounter: mockUsageCounter,
|
||||
connectorTokenClient,
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
actionTypeRegistry.register({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges,
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'system-connector-.cases',
|
||||
params: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
||||
expect(getKibanaPrivileges).toHaveBeenCalledWith({ params: { foo: 'bar' } });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2888,7 +3094,9 @@ describe('enqueueExecution()', () => {
|
|||
apiKey: null,
|
||||
source: asHttpRequestExecutionSource(request),
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -2910,7 +3118,9 @@ describe('enqueueExecution()', () => {
|
|||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
});
|
||||
});
|
||||
|
||||
test('tracks legacy RBAC', async () => {
|
||||
|
@ -2973,7 +3183,9 @@ describe('bulkEnqueueExecution()', () => {
|
|||
source: asHttpRequestExecutionSource(request),
|
||||
},
|
||||
]);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
|
@ -3005,7 +3217,9 @@ describe('bulkEnqueueExecution()', () => {
|
|||
])
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
});
|
||||
});
|
||||
|
||||
test('tracks legacy RBAC', async () => {
|
||||
|
@ -3341,7 +3555,7 @@ describe('getGlobalExecutionLogWithAuth()', () => {
|
|||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
await actionsClient.getGlobalExecutionLogWithAuth(opts);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to access logs', async () => {
|
||||
|
@ -3354,7 +3568,7 @@ describe('getGlobalExecutionLogWithAuth()', () => {
|
|||
`[Error: Unauthorized to access logs]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3396,7 +3610,7 @@ describe('getGlobalExecutionKpiWithAuth()', () => {
|
|||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
await actionsClient.getGlobalExecutionKpiWithAuth(opts);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to access kpi', async () => {
|
||||
|
@ -3409,7 +3623,7 @@ describe('getGlobalExecutionKpiWithAuth()', () => {
|
|||
`[Error: Unauthorized to access kpi]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ export class ActionsClient {
|
|||
const id = options?.id || SavedObjectsUtils.generateId();
|
||||
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('create', actionTypeId);
|
||||
await this.authorization.ensureAuthorized({ operation: 'create', actionTypeId });
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
|
@ -286,7 +286,7 @@ export class ActionsClient {
|
|||
*/
|
||||
public async update({ id, action }: UpdateOptions): Promise<ActionResult> {
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('update');
|
||||
await this.authorization.ensureAuthorized({ operation: 'update' });
|
||||
|
||||
const foundInMemoryConnector = this.inMemoryConnectors.find(
|
||||
(connector) => connector.id === id
|
||||
|
@ -396,7 +396,7 @@ export class ActionsClient {
|
|||
*/
|
||||
public async get({ id }: { id: string }): Promise<ActionResult> {
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
await this.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
|
@ -454,7 +454,7 @@ export class ActionsClient {
|
|||
*/
|
||||
public async getAll(): Promise<FindActionResult[]> {
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
await this.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
|
@ -502,7 +502,7 @@ export class ActionsClient {
|
|||
*/
|
||||
public async getBulk(ids: string[]): Promise<ActionResult[]> {
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
await this.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
ids.forEach((id) =>
|
||||
this.auditLogger?.log(
|
||||
|
@ -569,7 +569,7 @@ export class ActionsClient {
|
|||
configurationUtilities: ActionsConfigurationUtilities
|
||||
) {
|
||||
// Verify that user has edit access
|
||||
await this.authorization.ensureAuthorized('update');
|
||||
await this.authorization.ensureAuthorized({ operation: 'update' });
|
||||
|
||||
// Verify that token url is allowed by allowed hosts config
|
||||
try {
|
||||
|
@ -660,7 +660,7 @@ export class ActionsClient {
|
|||
*/
|
||||
public async delete({ id }: { id: string }) {
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('delete');
|
||||
await this.authorization.ensureAuthorized({ operation: 'delete' });
|
||||
|
||||
const foundInMemoryConnector = this.inMemoryConnectors.find(
|
||||
(connector) => connector.id === id
|
||||
|
@ -718,6 +718,21 @@ export class ActionsClient {
|
|||
return await this.unsecuredSavedObjectsClient.delete('action', id);
|
||||
}
|
||||
|
||||
private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) {
|
||||
const inMemoryConnector = this.inMemoryConnectors.find(
|
||||
(connector) => connector.id === connectorId
|
||||
);
|
||||
|
||||
const additionalPrivileges = inMemoryConnector?.isSystemAction
|
||||
? this.actionTypeRegistry.getSystemActionKibanaPrivileges(
|
||||
inMemoryConnector.actionTypeId,
|
||||
params
|
||||
)
|
||||
: [];
|
||||
|
||||
return additionalPrivileges;
|
||||
}
|
||||
|
||||
public async execute({
|
||||
actionId,
|
||||
params,
|
||||
|
@ -730,7 +745,8 @@ export class ActionsClient {
|
|||
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
) {
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params);
|
||||
await this.authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
|
||||
} else {
|
||||
trackLegacyRBACExemption('execute', this.usageCounter);
|
||||
}
|
||||
|
@ -751,7 +767,13 @@ export class ActionsClient {
|
|||
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
) {
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
/**
|
||||
* For scheduled executions the additional authorization check
|
||||
* for system actions (kibana privileges) will be performed
|
||||
* inside the ActionExecutor at execution time
|
||||
*/
|
||||
|
||||
await this.authorization.ensureAuthorized({ operation: 'execute' });
|
||||
} else {
|
||||
trackLegacyRBACExemption('enqueueExecution', this.usageCounter);
|
||||
}
|
||||
|
@ -765,12 +787,18 @@ export class ActionsClient {
|
|||
sources.push(option.source);
|
||||
}
|
||||
});
|
||||
|
||||
const authCounts = await getBulkAuthorizationModeBySource(
|
||||
this.unsecuredSavedObjectsClient,
|
||||
sources
|
||||
);
|
||||
if (authCounts[AuthorizationMode.RBAC] > 0) {
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
/**
|
||||
* For scheduled executions the additional authorization check
|
||||
* for system actions (kibana privileges) will be performed
|
||||
* inside the ActionExecutor at execution time
|
||||
*/
|
||||
await this.authorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
if (authCounts[AuthorizationMode.Legacy] > 0) {
|
||||
trackLegacyRBACExemption(
|
||||
|
@ -788,7 +816,7 @@ export class ActionsClient {
|
|||
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
) {
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
await this.authorization.ensureAuthorized({ operation: 'execute' });
|
||||
} else {
|
||||
trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter);
|
||||
}
|
||||
|
@ -831,7 +859,7 @@ export class ActionsClient {
|
|||
|
||||
const authorizationTuple = {} as KueryNode;
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
await this.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
|
@ -891,7 +919,7 @@ export class ActionsClient {
|
|||
|
||||
const authorizationTuple = {} as KueryNode;
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
await this.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
|
|
|
@ -18,6 +18,7 @@ import { AuthorizationMode } from './get_authorization_mode_by_source';
|
|||
const request = {} as KibanaRequest;
|
||||
|
||||
const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`;
|
||||
|
||||
function mockSecurity() {
|
||||
const security = securityMock.createSetup();
|
||||
const authorization = security.authz;
|
||||
|
@ -42,7 +43,7 @@ describe('ensureAuthorized', () => {
|
|||
request,
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
await actionsAuthorization.ensureAuthorized({ operation: 'create', actionTypeId: 'myType' });
|
||||
});
|
||||
|
||||
test('is a no-op when the security license is disabled', async () => {
|
||||
|
@ -53,7 +54,7 @@ describe('ensureAuthorized', () => {
|
|||
authorization,
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
await actionsAuthorization.ensureAuthorized({ operation: 'create', actionTypeId: 'myType' });
|
||||
});
|
||||
|
||||
test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => {
|
||||
|
@ -78,11 +79,11 @@ describe('ensureAuthorized', () => {
|
|||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
await actionsAuthorization.ensureAuthorized({ operation: 'create', actionTypeId: 'myType' });
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: mockAuthorizationAction('action', 'create'),
|
||||
kibana: [mockAuthorizationAction('action', 'create')],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -108,7 +109,7 @@ describe('ensureAuthorized', () => {
|
|||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('execute', 'myType');
|
||||
await actionsAuthorization.ensureAuthorized({ operation: 'execute', actionTypeId: 'myType' });
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
|
@ -153,7 +154,7 @@ describe('ensureAuthorized', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
actionsAuthorization.ensureAuthorized('create', 'myType')
|
||||
actionsAuthorization.ensureAuthorized({ operation: 'create', actionTypeId: 'myType' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized to create a \\"myType\\" action"`);
|
||||
});
|
||||
|
||||
|
@ -174,9 +175,57 @@ describe('ensureAuthorized', () => {
|
|||
username: 'some-user',
|
||||
} as unknown as AuthenticatedUser);
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('execute', 'myType');
|
||||
await actionsAuthorization.ensureAuthorized({ operation: 'execute', actionTypeId: 'myType' });
|
||||
|
||||
expect(authorization.actions.savedObject.get).not.toHaveBeenCalled();
|
||||
expect(checkPrivileges).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('checks additional privileges correctly', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = jest.fn();
|
||||
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
});
|
||||
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: true,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'execute'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
actionTypeId: 'myType',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
'get'
|
||||
);
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
'create'
|
||||
);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: [
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
'test/create',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,15 +28,14 @@ export interface ConstructorOptions {
|
|||
authorizationMode?: AuthorizationMode;
|
||||
}
|
||||
|
||||
const operationAlias: Record<
|
||||
string,
|
||||
(authorization: SecurityPluginSetup['authz']) => string | string[]
|
||||
> = {
|
||||
const operationAlias: Record<string, (authorization: SecurityPluginSetup['authz']) => string[]> = {
|
||||
execute: (authorization) => [
|
||||
authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
authorization.actions.savedObject.get(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
],
|
||||
list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'),
|
||||
list: (authorization) => [
|
||||
authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'),
|
||||
],
|
||||
};
|
||||
|
||||
const LEGACY_RBAC_EXEMPT_OPERATIONS = new Set(['get', 'execute']);
|
||||
|
@ -56,15 +55,26 @@ export class ActionsAuthorization {
|
|||
this.authorizationMode = authorizationMode;
|
||||
}
|
||||
|
||||
public async ensureAuthorized(operation: string, actionTypeId?: string) {
|
||||
public async ensureAuthorized({
|
||||
operation,
|
||||
actionTypeId,
|
||||
additionalPrivileges = [],
|
||||
}: {
|
||||
operation: string;
|
||||
actionTypeId?: string;
|
||||
additionalPrivileges?: string[];
|
||||
}) {
|
||||
const { authorization } = this;
|
||||
if (authorization?.mode?.useRbacForRequest(this.request)) {
|
||||
if (!this.isOperationExemptDueToLegacyRbac(operation)) {
|
||||
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
|
||||
|
||||
const privileges = operationAlias[operation]
|
||||
? operationAlias[operation](authorization)
|
||||
: [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)];
|
||||
|
||||
const { hasAllRequested } = await checkPrivileges({
|
||||
kibana: operationAlias[operation]
|
||||
? operationAlias[operation](authorization)
|
||||
: authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation),
|
||||
kibana: [...privileges, ...additionalPrivileges],
|
||||
});
|
||||
if (!hasAllRequested) {
|
||||
throw Boom.forbidden(
|
||||
|
|
|
@ -14,7 +14,7 @@ import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks';
|
||||
import { spacesServiceMock } from '@kbn/spaces-plugin/server/spaces_service/spaces_service.mock';
|
||||
import { ActionType } from '../types';
|
||||
import { actionsMock } from '../mocks';
|
||||
import { actionsAuthorizationMock, actionsMock } from '../mocks';
|
||||
import {
|
||||
asHttpRequestExecutionSource,
|
||||
asSavedObjectExecutionSource,
|
||||
|
@ -43,6 +43,9 @@ const loggerMock: ReturnType<typeof loggingSystemMock.createLogger> =
|
|||
loggingSystemMock.createLogger();
|
||||
const securityMockStart = securityMock.createStart();
|
||||
|
||||
const authorizationMock = actionsAuthorizationMock.create();
|
||||
const getActionsAuthorizationWithRequest = jest.fn();
|
||||
|
||||
actionExecutor.initialize({
|
||||
logger: loggerMock,
|
||||
spaces: spacesMock,
|
||||
|
@ -51,6 +54,7 @@ actionExecutor.initialize({
|
|||
actionTypeRegistry,
|
||||
encryptedSavedObjectsClient,
|
||||
eventLogger,
|
||||
getActionsAuthorizationWithRequest,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'preconfigured',
|
||||
|
@ -95,6 +99,8 @@ beforeEach(() => {
|
|||
roles: ['superuser'],
|
||||
username: 'coolguy',
|
||||
}));
|
||||
|
||||
getActionsAuthorizationWithRequest.mockReturnValue(authorizationMock);
|
||||
});
|
||||
|
||||
test('successfully executes', async () => {
|
||||
|
@ -681,6 +687,7 @@ test('successfully executes with system connector', async () => {
|
|||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: true,
|
||||
validate: {
|
||||
config: { schema: schema.any() },
|
||||
secrets: { schema: schema.any() },
|
||||
|
@ -802,6 +809,92 @@ test('successfully executes with system connector', async () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('successfully authorize system actions', async () => {
|
||||
const actionType: jest.Mocked<ActionType> = {
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: () => ['test/create'],
|
||||
isSystemActionType: true,
|
||||
validate: {
|
||||
config: { schema: schema.any() },
|
||||
secrets: { schema: schema.any() },
|
||||
params: { schema: schema.any() },
|
||||
},
|
||||
executor: jest.fn(),
|
||||
};
|
||||
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.isSystemActionType.mockReturnValueOnce(true);
|
||||
actionTypeRegistry.getSystemActionKibanaPrivileges.mockReturnValueOnce(['test/create']);
|
||||
|
||||
await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' });
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
});
|
||||
|
||||
test('pass the params to the actionTypeRegistry when authorizing system actions', async () => {
|
||||
const actionType: jest.Mocked<ActionType> = {
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
getKibanaPrivileges: () => ['test/create'],
|
||||
isSystemActionType: true,
|
||||
validate: {
|
||||
config: { schema: schema.any() },
|
||||
secrets: { schema: schema.any() },
|
||||
params: { schema: schema.any() },
|
||||
},
|
||||
executor: jest.fn(),
|
||||
};
|
||||
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.isSystemActionType.mockReturnValueOnce(true);
|
||||
actionTypeRegistry.getSystemActionKibanaPrivileges.mockReturnValueOnce(['test/create']);
|
||||
|
||||
await actionExecutor.execute({
|
||||
...executeParams,
|
||||
params: { foo: 'bar' },
|
||||
actionId: 'system-connector-.cases',
|
||||
});
|
||||
|
||||
expect(actionTypeRegistry.getSystemActionKibanaPrivileges).toHaveBeenCalledWith('.cases', {
|
||||
foo: 'bar',
|
||||
});
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
});
|
||||
|
||||
test('does not authorize non system actions', async () => {
|
||||
const actionType: jest.Mocked<ActionType> = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
minimumLicenseRequired: 'basic',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
validate: {
|
||||
config: { schema: schema.object({ bar: schema.string() }) },
|
||||
secrets: { schema: schema.object({ apiKey: schema.string() }) },
|
||||
params: { schema: schema.object({ foo: schema.boolean() }) },
|
||||
},
|
||||
executor: jest.fn(),
|
||||
};
|
||||
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.isSystemActionType.mockReturnValueOnce(false);
|
||||
|
||||
await actionExecutor.execute({ ...executeParams, actionId: 'preconfigured' });
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('successfully executes as a task', async () => {
|
||||
const actionType: jest.Mocked<ActionType> = {
|
||||
id: 'test',
|
||||
|
@ -1102,6 +1195,7 @@ test('should not throws an error if actionType is system action', async () => {
|
|||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: true,
|
||||
validate: {
|
||||
config: { schema: schema.any() },
|
||||
secrets: { schema: schema.any() },
|
||||
|
@ -1151,6 +1245,7 @@ test('throws an error when passing isESOCanEncrypt with value of false', async (
|
|||
encryptedSavedObjectsClient,
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
inMemoryConnectors: [],
|
||||
getActionsAuthorizationWithRequest,
|
||||
});
|
||||
await expect(
|
||||
customActionExecutor.execute(executeParams)
|
||||
|
@ -1168,6 +1263,7 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f
|
|||
actionTypeRegistry,
|
||||
encryptedSavedObjectsClient,
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
getActionsAuthorizationWithRequest,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'preconfigured',
|
||||
|
@ -1318,6 +1414,7 @@ test('should not throw error if action is system action and isESOCanEncrypt is f
|
|||
actionTypeRegistry,
|
||||
encryptedSavedObjectsClient,
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
getActionsAuthorizationWithRequest,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
actionTypeId: '.cases',
|
||||
|
@ -1337,6 +1434,7 @@ test('should not throw error if action is system action and isESOCanEncrypt is f
|
|||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: true,
|
||||
validate: {
|
||||
config: { schema: schema.any() },
|
||||
secrets: { schema: schema.any() },
|
||||
|
|
|
@ -36,6 +36,7 @@ import { ActionExecutionSource } from './action_execution_source';
|
|||
import { RelatedSavedObjects } from './related_saved_objects';
|
||||
import { createActionEventLogRecordObject } from './create_action_event_log_record_object';
|
||||
import { ActionExecutionError, ActionExecutionErrorReason } from './errors/action_execution_error';
|
||||
import type { ActionsAuthorization } from '../authorization/actions_authorization';
|
||||
|
||||
// 1,000,000 nanoseconds in 1 millisecond
|
||||
const Millis2Nanos = 1000 * 1000;
|
||||
|
@ -49,6 +50,7 @@ export interface ActionExecutorContext {
|
|||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
eventLogger: IEventLogger;
|
||||
inMemoryConnectors: InMemoryConnector[];
|
||||
getActionsAuthorizationWithRequest: (request: KibanaRequest) => ActionsAuthorization;
|
||||
}
|
||||
|
||||
export interface TaskInfo {
|
||||
|
@ -108,6 +110,7 @@ export class ActionExecutor {
|
|||
if (!this.isInitialized) {
|
||||
throw new Error('ActionExecutor not initialized');
|
||||
}
|
||||
|
||||
return withSpan(
|
||||
{
|
||||
name: `execute_action`,
|
||||
|
@ -117,12 +120,19 @@ export class ActionExecutor {
|
|||
},
|
||||
},
|
||||
async (span) => {
|
||||
const { spaces, getServices, actionTypeRegistry, eventLogger, security } =
|
||||
this.actionExecutorContext!;
|
||||
const {
|
||||
spaces,
|
||||
getServices,
|
||||
actionTypeRegistry,
|
||||
eventLogger,
|
||||
security,
|
||||
getActionsAuthorizationWithRequest,
|
||||
} = this.actionExecutorContext!;
|
||||
|
||||
const services = getServices(request);
|
||||
const spaceId = spaces && spaces.getSpaceId(request);
|
||||
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};
|
||||
const authorization = getActionsAuthorizationWithRequest(request);
|
||||
|
||||
const actionInfo =
|
||||
actionInfoFromTaskRunner ||
|
||||
|
@ -223,6 +233,19 @@ export class ActionExecutor {
|
|||
|
||||
let rawResult: ActionTypeExecutorRawResult<unknown>;
|
||||
try {
|
||||
/**
|
||||
* Ensures correct permissions for execution and
|
||||
* performs authorization checks for system actions.
|
||||
* It will thrown an error in case of failure.
|
||||
*/
|
||||
await ensureAuthorizedToExecute({
|
||||
params,
|
||||
actionId,
|
||||
actionTypeId,
|
||||
actionTypeRegistry,
|
||||
authorization,
|
||||
});
|
||||
|
||||
rawResult = await actionType.executor({
|
||||
actionId,
|
||||
services,
|
||||
|
@ -236,7 +259,10 @@ export class ActionExecutor {
|
|||
source,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.reason === ActionExecutionErrorReason.Validation) {
|
||||
if (
|
||||
err.reason === ActionExecutionErrorReason.Validation ||
|
||||
err.reason === ActionExecutionErrorReason.Authorization
|
||||
) {
|
||||
rawResult = err.result;
|
||||
} else {
|
||||
rawResult = {
|
||||
|
@ -507,3 +533,37 @@ function validateAction(
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface EnsureAuthorizedToExecuteOpts {
|
||||
actionId: string;
|
||||
actionTypeId: string;
|
||||
params: Record<string, unknown>;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
authorization: ActionsAuthorization;
|
||||
}
|
||||
|
||||
const ensureAuthorizedToExecute = async ({
|
||||
actionId,
|
||||
actionTypeId,
|
||||
params,
|
||||
actionTypeRegistry,
|
||||
authorization,
|
||||
}: EnsureAuthorizedToExecuteOpts) => {
|
||||
try {
|
||||
if (actionTypeRegistry.isSystemActionType(actionTypeId)) {
|
||||
const additionalPrivileges = actionTypeRegistry.getSystemActionKibanaPrivileges(
|
||||
actionTypeId,
|
||||
params
|
||||
);
|
||||
|
||||
await authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ActionExecutionError(error.message, ActionExecutionErrorReason.Authorization, {
|
||||
actionId,
|
||||
status: 'error',
|
||||
message: error.message,
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ActionTypeExecutorResult } from '../../types';
|
|||
|
||||
export enum ActionExecutionErrorReason {
|
||||
Validation = 'validation',
|
||||
Authorization = 'authorization',
|
||||
}
|
||||
|
||||
export class ActionExecutionError extends Error {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from '@kbn/core/server/mocks';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks';
|
||||
import { ActionTypeDisabledError } from './errors';
|
||||
import { actionsClientMock } from '../mocks';
|
||||
import { actionsAuthorizationMock } from '../mocks';
|
||||
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
|
||||
import { IN_MEMORY_METRICS } from '../monitoring';
|
||||
import { pick } from 'lodash';
|
||||
|
@ -106,15 +106,17 @@ const services = {
|
|||
log: jest.fn(),
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
};
|
||||
|
||||
const actionExecutorInitializerParams = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getServices: jest.fn().mockReturnValue(services),
|
||||
actionTypeRegistry,
|
||||
getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()),
|
||||
getActionsAuthorizationWithRequest: jest.fn().mockReturnValue(actionsAuthorizationMock.create()),
|
||||
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
|
||||
eventLogger,
|
||||
inMemoryConnectors: [],
|
||||
};
|
||||
|
||||
const taskRunnerFactoryInitializerParams = {
|
||||
spaceIdToNamespace,
|
||||
actionTypeRegistry,
|
||||
|
|
|
@ -513,6 +513,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
encryptedSavedObjectsClient,
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
inMemoryConnectors: this.inMemoryConnectors,
|
||||
getActionsAuthorizationWithRequest(request: KibanaRequest) {
|
||||
return instantiateAuthorization(request);
|
||||
},
|
||||
});
|
||||
|
||||
taskRunnerFactory!.initialize({
|
||||
|
|
|
@ -151,6 +151,18 @@ export interface ActionType<
|
|||
connector?: (config: Config, secrets: Secrets) => string | null;
|
||||
};
|
||||
isSystemActionType?: boolean;
|
||||
/**
|
||||
* Additional Kibana privileges to be checked by the actions framework.
|
||||
* Use it if you want to perform extra authorization checks based on a Kibana feature.
|
||||
* For example, you can define the privileges a users needs to have to execute
|
||||
* a Case or OsQuery system action.
|
||||
*
|
||||
* The list of the privileges follows the Kibana privileges format usually generated with `security.authz.actions.*.get(...)`.
|
||||
*
|
||||
* It only works with system actions and only when executing an action.
|
||||
* For all other scenarios they will be ignored
|
||||
*/
|
||||
getKibanaPrivileges?: (args?: { params?: Params }) => string[];
|
||||
renderParameterTemplates?: RenderParameterTemplates<Params>;
|
||||
executor: ExecutorType<Config, Secrets, Params, ExecutorResultData>;
|
||||
}
|
||||
|
|
|
@ -576,7 +576,7 @@ async function ensureAuthorizationForBulkUpdate(
|
|||
const { field } = operation;
|
||||
if (field === 'snoozeSchedule' || field === 'apiKey') {
|
||||
try {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
break;
|
||||
} catch (error) {
|
||||
throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`);
|
||||
|
|
|
@ -134,7 +134,7 @@ const bulkEnableRulesWithOCC = async (
|
|||
try {
|
||||
if (rule.attributes.actions.length) {
|
||||
try {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
} catch (error) {
|
||||
throw Error(`Rule not authorized for bulk enable - ${error.message}`);
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -37,7 +37,7 @@ async function muteAllWithOCC(context: RulesClientContext, { id }: { id: string
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -42,7 +42,7 @@ async function muteInstanceWithOCC(
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function runSoon(context: RulesClientContext, { id }: { id: string
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -62,7 +62,7 @@ async function snoozeWithOCC(
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -40,7 +40,7 @@ async function unmuteAllWithOCC(context: RulesClientContext, { id }: { id: strin
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -47,7 +47,7 @@ async function unmuteInstanceWithOCC(
|
|||
entity: AlertingAuthorizationEntity.Rule,
|
||||
});
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -45,7 +45,7 @@ async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }:
|
|||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -58,7 +58,7 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st
|
|||
entity: AlertingAuthorizationEntity.Rule,
|
||||
});
|
||||
if (attributes.actions.length) {
|
||||
await context.actionsAuthorization.ensureAuthorized('execute');
|
||||
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
|
||||
}
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -148,7 +148,7 @@ describe('enable()', () => {
|
|||
operation: 'enable',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to enable this type of alert', async () => {
|
||||
|
|
|
@ -135,7 +135,7 @@ describe('muteAll()', () => {
|
|||
operation: 'muteAll',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to muteAll this type of alert', async () => {
|
||||
|
|
|
@ -160,7 +160,7 @@ describe('muteInstance()', () => {
|
|||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
|
||||
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -115,7 +115,7 @@ describe('runSoon()', () => {
|
|||
operation: 'runSoon',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to run this type of rule ad hoc', async () => {
|
||||
|
|
|
@ -135,7 +135,7 @@ describe('unmuteAll()', () => {
|
|||
operation: 'unmuteAll',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to unmuteAll this type of alert', async () => {
|
||||
|
|
|
@ -158,7 +158,7 @@ describe('unmuteInstance()', () => {
|
|||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
|
||||
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -355,7 +355,7 @@ describe('updateApiKey()', () => {
|
|||
test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => {
|
||||
await rulesClient.updateApiKey({ id: '1' });
|
||||
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' });
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
|
|
|
@ -65,6 +65,7 @@ const enabledActionTypes = [
|
|||
'test.excluded',
|
||||
'test.capped',
|
||||
'test.system-action',
|
||||
'test.system-action-kibana-privileges',
|
||||
];
|
||||
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
|
|
|
@ -74,7 +74,12 @@ export function defineActionTypes(
|
|||
actions.registerType(getNoAttemptsRateLimitedActionType());
|
||||
actions.registerType(getAuthorizationActionType(core));
|
||||
actions.registerType(getExcludedActionType());
|
||||
|
||||
/**
|
||||
* System actions
|
||||
*/
|
||||
actions.registerType(getSystemActionType());
|
||||
actions.registerType(getSystemActionTypeWithKibanaPrivileges());
|
||||
|
||||
/** Sub action framework */
|
||||
|
||||
|
@ -426,3 +431,56 @@ function getSystemActionType() {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSystemActionTypeWithKibanaPrivileges() {
|
||||
const result: ActionType<{}, {}, { index?: string; reference?: string }> = {
|
||||
id: 'test.system-action-kibana-privileges',
|
||||
name: 'Test system action with kibana privileges',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
/**
|
||||
* Requires all access to the case feature
|
||||
* in Stack management
|
||||
*/
|
||||
getKibanaPrivileges: () => ['cases:cases/createCase'],
|
||||
validate: {
|
||||
params: {
|
||||
schema: schema.any(),
|
||||
},
|
||||
config: {
|
||||
schema: schema.any(),
|
||||
},
|
||||
secrets: {
|
||||
schema: schema.any(),
|
||||
},
|
||||
},
|
||||
isSystemActionType: true,
|
||||
/**
|
||||
* The executor writes a doc to the
|
||||
* testing index. The test uses the doc
|
||||
* to verify that the action is executed
|
||||
* correctly
|
||||
*/
|
||||
async executor({ params, services, actionId }) {
|
||||
const { index, reference } = params;
|
||||
|
||||
if (index == null || reference == null) {
|
||||
return { status: 'ok', actionId };
|
||||
}
|
||||
|
||||
await services.scopedClusterClient.index({
|
||||
index,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
params,
|
||||
reference,
|
||||
source: 'action:test.system-action-kibana-privileges',
|
||||
},
|
||||
});
|
||||
|
||||
return { status: 'ok', actionId };
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -350,6 +350,62 @@ export function defineRoutes(
|
|||
});
|
||||
return res.noContent();
|
||||
} catch (err) {
|
||||
if (err.isBoom && err.output.statusCode === 403) {
|
||||
return res.forbidden({ body: err });
|
||||
}
|
||||
|
||||
return res.badRequest({ body: err });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: '/api/alerts_fixture/{id}/bulk_enqueue_actions',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
params: schema.recordOf(schema.string(), schema.any()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> => {
|
||||
try {
|
||||
const [, { actions, security, spaces }] = await core.getStartServices();
|
||||
const actionsClient = await actions.getActionsClientWithRequest(req);
|
||||
|
||||
const createAPIKeyResult =
|
||||
security &&
|
||||
(await security.authc.apiKeys.grantAsInternalUser(req, {
|
||||
name: `alerts_fixture:bulk_enqueue_actions:${uuidv4()}`,
|
||||
role_descriptors: {},
|
||||
}));
|
||||
|
||||
await actionsClient.bulkEnqueueExecution([
|
||||
{
|
||||
id: req.params.id,
|
||||
spaceId: spaces ? spaces.spacesService.getSpaceId(req) : 'default',
|
||||
executionId: uuidv4(),
|
||||
apiKey: createAPIKeyResult
|
||||
? Buffer.from(`${createAPIKeyResult.id}:${createAPIKeyResult.api_key}`).toString(
|
||||
'base64'
|
||||
)
|
||||
: null,
|
||||
params: req.body.params,
|
||||
},
|
||||
]);
|
||||
return res.noContent();
|
||||
} catch (err) {
|
||||
if (err.isBoom && err.output.statusCode === 403) {
|
||||
return res.forbidden({ body: err });
|
||||
}
|
||||
|
||||
return res.badRequest({ body: err });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/server';
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { getEventLog, getUrlPrefix, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
|
||||
describe('bulk_enqueue', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
before(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
|
||||
const { user, space } = scenario;
|
||||
|
||||
it(`should handle enqueue request appropriately: ${scenario.id}`, async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
const connectorId = createdAction.id;
|
||||
const name = 'My action';
|
||||
const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/bulk_enqueue_actions`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { reference, index: ES_TEST_INDEX_NAME, message: 'Testing 123' },
|
||||
});
|
||||
|
||||
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.status).to.eql(403);
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.index-record:${connectorId}: ${name}`,
|
||||
startDate,
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should authorize system actions correctly: ${scenario.id}`, async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const name = 'System action: test.system-action-kibana-privileges';
|
||||
const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/bulk_enqueue_actions`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { index: ES_TEST_INDEX_NAME, reference },
|
||||
});
|
||||
|
||||
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.status).to.eql(403);
|
||||
break;
|
||||
/**
|
||||
* The users in these scenarios have access
|
||||
* to Actions but do not have access to
|
||||
* the system action. They should be able to
|
||||
* enqueue the action but the execution should fail.
|
||||
*/
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'failure',
|
||||
message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
errorMessage: 'Unauthorized to execute actions',
|
||||
startDate,
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* The users in these scenarios have access
|
||||
* to Actions and to the system action. They should be able to
|
||||
* enqueue the action and the execution should succeed.
|
||||
*/
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
startDate,
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
'action:test.system-action-kibana-privileges',
|
||||
reference,
|
||||
1
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
connectorId: string;
|
||||
outcome: string;
|
||||
message: string;
|
||||
startDate: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const validateEventLog = async (params: ValidateEventLogParams): Promise<void> => {
|
||||
const { spaceId, connectorId, outcome, message, startDate, errorMessage } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
const events_ = await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'action',
|
||||
id: connectorId,
|
||||
provider: 'actions',
|
||||
actions: new Map([['execute', { gte: 1 }]]),
|
||||
});
|
||||
|
||||
const filteredEvents = events_.filter((event) => event!['@timestamp']! >= startDate);
|
||||
if (filteredEvents.length < 1) throw new Error('no recent events found yet');
|
||||
|
||||
return filteredEvents;
|
||||
});
|
||||
|
||||
expect(events.length).to.be(1);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
expect(event?.event?.outcome).to.eql(outcome);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/server';
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types';
|
||||
import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { getEventLog, getUrlPrefix, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
|
||||
describe('enqueue', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
before(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
|
||||
const { user, space } = scenario;
|
||||
|
||||
it(`should handle enqueue request appropriately: ${scenario.id}`, async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
const connectorId = createdAction.id;
|
||||
const name = 'My action';
|
||||
const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/enqueue_action`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { reference, index: ES_TEST_INDEX_NAME, message: 'Testing 123' },
|
||||
});
|
||||
|
||||
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.status).to.eql(403);
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.index-record:${connectorId}: ${name}`,
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
startDate,
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should authorize system actions correctly: ${scenario.id}`, async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const name = 'System action: test.system-action-kibana-privileges';
|
||||
const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts_fixture/${connectorId}/enqueue_action`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { index: ES_TEST_INDEX_NAME, reference },
|
||||
});
|
||||
|
||||
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.status).to.eql(403);
|
||||
break;
|
||||
/**
|
||||
* The users in these scenarios have access
|
||||
* to Actions but do not have access to
|
||||
* the system action. They should be able to
|
||||
* enqueue the action but the execution should fail.
|
||||
*/
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'failure',
|
||||
message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
errorMessage: 'Unauthorized to execute actions',
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
startDate,
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* The users in these scenarios have access
|
||||
* to Actions and to the system action. They should be able to
|
||||
* enqueue the action and the execution should succeed.
|
||||
*/
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
startDate,
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
'action:test.system-action-kibana-privileges',
|
||||
reference,
|
||||
1
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
connectorId: string;
|
||||
outcome: string;
|
||||
message: string;
|
||||
startDate: string;
|
||||
errorMessage?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
const validateEventLog = async (params: ValidateEventLogParams): Promise<void> => {
|
||||
const { spaceId, connectorId, outcome, message, startDate, errorMessage, source } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
const events_ = await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'action',
|
||||
id: connectorId,
|
||||
provider: 'actions',
|
||||
actions: new Map([['execute', { gte: 1 }]]),
|
||||
});
|
||||
|
||||
const filteredEvents = events_.filter((event) => event!['@timestamp']! >= startDate);
|
||||
if (filteredEvents.length < 1) throw new Error('no recent events found yet');
|
||||
|
||||
return filteredEvents;
|
||||
});
|
||||
|
||||
expect(events.length).to.be(1);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
expect(event?.event?.outcome).to.eql(outcome);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
|
||||
if (source) {
|
||||
expect(event?.kibana?.action?.execution?.source).to.eql(source.toLowerCase());
|
||||
}
|
||||
};
|
||||
}
|
|
@ -9,7 +9,7 @@ import expect from '@kbn/expect';
|
|||
import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server';
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/lib/action_execution_source';
|
||||
import { UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
|
@ -37,7 +37,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
for (const scenario of [...UserAtSpaceScenarios, systemActionScenario]) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle execute request appropriately', async () => {
|
||||
|
@ -85,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.be.an('object');
|
||||
const searchResult = await esTestIndexTool.search(
|
||||
|
@ -169,6 +170,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
|
@ -240,6 +242,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.be.an('object');
|
||||
const searchResult = await esTestIndexTool.search(
|
||||
|
@ -294,6 +297,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
|
@ -321,6 +325,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
|
@ -399,6 +404,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
break;
|
||||
default:
|
||||
|
@ -448,6 +454,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
searchResult = await esTestIndexTool.search('action:test.authorization', reference);
|
||||
expect(searchResult.body.hits.total.value).to.eql(1);
|
||||
|
@ -493,6 +500,69 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should authorize system actions correctly', async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const name = 'System action: test.system-action-kibana-privileges';
|
||||
const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`;
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector/${connectorId}/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { index: ES_TEST_INDEX_NAME, reference },
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
/**
|
||||
* The users in these scenarios may have access
|
||||
* to Actions but do not have access to
|
||||
* the system action. They should not be able to
|
||||
* to execute even if they have access to Actions.
|
||||
*/
|
||||
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':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute actions',
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* The users in these scenarios have access
|
||||
* to Actions and to the system action. They should be able to
|
||||
* execute.
|
||||
*/
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
|
||||
await validateSystemEventLog({
|
||||
spaceId: space.id,
|
||||
connectorId,
|
||||
startDate,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
});
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
'action:test.system-action-kibana-privileges',
|
||||
reference,
|
||||
1
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -505,10 +575,20 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
message: string;
|
||||
errorMessage?: string;
|
||||
source?: string;
|
||||
spaceAgnostic?: boolean;
|
||||
}
|
||||
|
||||
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
|
||||
const { spaceId, connectorId, actionTypeId, outcome, message, errorMessage, source } = params;
|
||||
const {
|
||||
spaceId,
|
||||
connectorId,
|
||||
actionTypeId,
|
||||
outcome,
|
||||
message,
|
||||
errorMessage,
|
||||
source,
|
||||
spaceAgnostic,
|
||||
} = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
|
@ -521,7 +601,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
// filter: 'event.action:(execute)',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -555,6 +634,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
id: connectorId,
|
||||
namespace: 'space1',
|
||||
type_id: actionTypeId,
|
||||
...(spaceAgnostic ? { space_agnostic: true } : {}),
|
||||
},
|
||||
]);
|
||||
expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
|
||||
|
@ -569,43 +649,42 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
if (errorMessage) {
|
||||
expect(executeEvent?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
|
||||
// const event = events[0];
|
||||
|
||||
// const duration = event?.event?.duration;
|
||||
// const eventStart = Date.parse(event?.event?.start || 'undefined');
|
||||
// const eventEnd = Date.parse(event?.event?.end || 'undefined');
|
||||
// const dateNow = Date.now();
|
||||
|
||||
// expect(typeof duration).to.be('number');
|
||||
// expect(eventStart).to.be.ok();
|
||||
// expect(eventEnd).to.be.ok();
|
||||
|
||||
// const durationDiff = Math.abs(
|
||||
// Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
|
||||
// );
|
||||
|
||||
// // account for rounding errors
|
||||
// expect(durationDiff < 1).to.equal(true);
|
||||
// expect(eventStart <= eventEnd).to.equal(true);
|
||||
// expect(eventEnd <= dateNow).to.equal(true);
|
||||
|
||||
// expect(event?.event?.outcome).to.equal(outcome);
|
||||
|
||||
// expect(event?.kibana?.saved_objects).to.eql([
|
||||
// {
|
||||
// rel: 'primary',
|
||||
// type: 'action',
|
||||
// id: connectorId,
|
||||
// type_id: actionTypeId,
|
||||
// namespace: spaceId,
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// expect(event?.message).to.eql(message);
|
||||
|
||||
// if (errorMessage) {
|
||||
// expect(event?.error?.message).to.eql(errorMessage);
|
||||
// }
|
||||
}
|
||||
|
||||
const validateSystemEventLog = async (
|
||||
params: Omit<ValidateEventLogParams, 'actionTypeId' | 'spaceAgnostic'> & { startDate: string }
|
||||
): Promise<void> => {
|
||||
const { spaceId, connectorId, outcome, message, startDate, errorMessage, source } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
const events_ = await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'action',
|
||||
id: connectorId,
|
||||
provider: 'actions',
|
||||
actions: new Map([['execute', { gte: 1 }]]),
|
||||
});
|
||||
|
||||
const filteredEvents = events_.filter((event) => event!['@timestamp']! >= startDate);
|
||||
if (filteredEvents.length < 1) throw new Error('no recent events found yet');
|
||||
|
||||
return filteredEvents;
|
||||
});
|
||||
|
||||
expect(events.length).to.be(1);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
expect(event?.event?.outcome).to.eql(outcome);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
|
||||
if (source) {
|
||||
expect(event?.kibana?.action?.execution?.source).to.eql(source.toLowerCase());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -134,6 +134,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
connector_type_id: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
is_deprecated: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
is_preconfigured: true,
|
||||
|
@ -303,6 +312,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
connector_type_id: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
is_deprecated: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
is_preconfigured: true,
|
||||
|
@ -435,6 +453,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
connector_type_id: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
is_deprecated: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
is_preconfigured: true,
|
||||
|
|
|
@ -18,6 +18,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
|
|||
after(async () => {
|
||||
await tearDown(getService);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./connector_types/oauth_access_token'));
|
||||
loadTestFile(require.resolve('./connector_types/cases_webhook'));
|
||||
loadTestFile(require.resolve('./connector_types/jira'));
|
||||
|
@ -47,10 +48,13 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
|
|||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./connector_types'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./enqueue'));
|
||||
loadTestFile(require.resolve('./bulk_enqueue'));
|
||||
|
||||
/**
|
||||
* Sub action framework
|
||||
*/
|
||||
// loadTestFile(require.resolve('./sub_action_framework'));
|
||||
|
||||
loadTestFile(require.resolve('./sub_action_framework'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -154,6 +154,42 @@ const Space1AllWithRestrictedFixture: User = {
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This user is needed to test system actions.
|
||||
* In x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
|
||||
* we registered a system action type which requires access to Cases. This user has
|
||||
* access to Cases only in the Stack Management. The tests use this user to
|
||||
* execute the system action and verify that the authorization is performed
|
||||
* as expected
|
||||
*/
|
||||
const CasesAll: User = {
|
||||
username: 'cases_all',
|
||||
fullName: 'cases_all',
|
||||
password: 'cases_all',
|
||||
role: {
|
||||
name: 'cases_all_role',
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: [`${ES_TEST_INDEX_NAME}*`],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
generalCases: ['all'],
|
||||
actions: ['all'],
|
||||
alertsFixture: ['all'],
|
||||
alertsRestrictedFixture: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Users: User[] = [
|
||||
NoKibanaPrivileges,
|
||||
Superuser,
|
||||
|
@ -161,6 +197,7 @@ export const Users: User[] = [
|
|||
Space1All,
|
||||
Space1AllWithRestrictedFixture,
|
||||
Space1AllAlertingNoneActions,
|
||||
CasesAll,
|
||||
];
|
||||
|
||||
const Space1: Space = {
|
||||
|
@ -254,6 +291,16 @@ const Space1AllAtSpace2: Space1AllAtSpace2 = {
|
|||
space: Space2,
|
||||
};
|
||||
|
||||
interface SystemActionSpace1 extends Scenario {
|
||||
id: 'system_actions at space1';
|
||||
}
|
||||
|
||||
export const systemActionScenario: SystemActionSpace1 = {
|
||||
id: 'system_actions at space1',
|
||||
user: CasesAll,
|
||||
space: Space1,
|
||||
};
|
||||
|
||||
export const UserAtSpaceScenarios: [
|
||||
NoKibanaPrivilegesAtSpace1,
|
||||
SuperuserAtSpace1,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
|
||||
describe('bulk_enqueue', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
before(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
});
|
||||
after(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
it('should handle bulk_enqueue request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
const reference = `actions-enqueue-1:${Spaces.space1.id}:${createdAction.id}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${
|
||||
createdAction.id
|
||||
}/bulk_enqueue_actions`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { reference, index: ES_TEST_INDEX_NAME, message: 'Testing 123' },
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(204);
|
||||
await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1);
|
||||
});
|
||||
|
||||
it('should enqueue system actions correctly', async () => {
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const reference = `actions-enqueue-1:${Spaces.space1.id}:${connectorId}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${connectorId}/bulk_enqueue_actions`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { index: ES_TEST_INDEX_NAME, reference },
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
'action:test.system-action-kibana-privileges',
|
||||
reference,
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -201,5 +201,25 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(total).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should enqueue system actions correctly', async () => {
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const reference = `actions-enqueue-1:${Spaces.space1.id}:${connectorId}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${connectorId}/enqueue_action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { index: ES_TEST_INDEX_NAME, reference },
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(204);
|
||||
|
||||
await esTestIndexTool.waitForDocs(
|
||||
'action:test.system-action-kibana-privileges',
|
||||
reference,
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -329,6 +329,56 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute system actions correctly', async () => {
|
||||
const connectorId = 'system-connector-test.system-action';
|
||||
const name = 'System action: test.system-action';
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${connectorId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: Spaces.space1.id,
|
||||
actionId: connectorId,
|
||||
actionTypeId: 'test.system-action',
|
||||
outcome: 'success',
|
||||
message: `action executed: test.system-action:${connectorId}: ${name}`,
|
||||
startMessage: `action started: test.system-action:${connectorId}: ${name}`,
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
spaceAgnostic: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute system actions with kibana privileges correctly', async () => {
|
||||
const connectorId = 'system-connector-test.system-action-kibana-privileges';
|
||||
const name = 'System action: test.system-action-kibana-privileges';
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${connectorId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: Spaces.space1.id,
|
||||
actionId: connectorId,
|
||||
actionTypeId: 'test.system-action-kibana-privileges',
|
||||
outcome: 'success',
|
||||
message: `action executed: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
startMessage: `action started: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
source: ActionExecutionSourceType.HTTP_REQUEST,
|
||||
spaceAgnostic: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
|
@ -340,6 +390,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
errorMessage?: string;
|
||||
startMessage?: string;
|
||||
source?: string;
|
||||
spaceAgnostic?: boolean;
|
||||
}
|
||||
|
||||
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
|
||||
|
@ -352,6 +403,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
startMessage,
|
||||
errorMessage,
|
||||
source,
|
||||
spaceAgnostic,
|
||||
} = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
|
@ -398,6 +450,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
id: actionId,
|
||||
namespace: 'space1',
|
||||
type_id: actionTypeId,
|
||||
...(spaceAgnostic ? { space_agnostic: true } : {}),
|
||||
},
|
||||
]);
|
||||
expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
|
||||
|
|
|
@ -123,6 +123,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
connector_type_id: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
is_deprecated: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
is_preconfigured: true,
|
||||
|
@ -244,6 +253,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
connector_type_id: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
is_deprecated: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
is_preconfigured: true,
|
||||
|
@ -379,6 +397,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
|
|||
name: 'System action: test.system-action',
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
actionTypeId: 'test.system-action-kibana-privileges',
|
||||
id: 'system-connector-test.system-action-kibana-privileges',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
name: 'System action: test.system-action-kibana-privileges',
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
id: 'custom-system-abc-connector',
|
||||
isPreconfigured: true,
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo
|
|||
loadTestFile(require.resolve('./monitoring_collection'));
|
||||
loadTestFile(require.resolve('./execute'));
|
||||
loadTestFile(require.resolve('./enqueue'));
|
||||
loadTestFile(require.resolve('./bulk_enqueue'));
|
||||
loadTestFile(require.resolve('./connector_types/stack/email'));
|
||||
loadTestFile(require.resolve('./connector_types/stack/email_html'));
|
||||
loadTestFile(require.resolve('./connector_types/stack/es_index'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue