mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Adds Role Based Access-Control to the Alerting & Action plugins based on Kibana Feature Controls (#67157)
This PR adds _Role Based Access-Control_ to the Alerting framework & Actions feature using Kibana Feature Controls, addressing most of the Meta issue: https://github.com/elastic/kibana/issues/43994 This also closes https://github.com/elastic/kibana/issues/62438 This PR includes the following: 1. Adds `alerting` specific Security Actions (not to be confused with Alerting Actions) to the `security` plugin which allows us to assign alerting specific privileges to users of other plugins using the `features` plugin. 2. Removes the security wrapper from the savedObjectsClient in AlertsClient and instead plugs in the new AlertsAuthorization which performs the privilege checks on each api call made to the AlertsClient. 3. Adds privileges in each plugin that is already using the Alerting Framework which mirror (as closely as possible) the existing api-level tag-based privileges and plugs them into the AlertsClient. 4. Adds feature granted privileges arounds Actions (by relying on Saved Object privileges under the hood) and plugs them into the ActionsClient 5. Removes the legacy api-level tag-based privilege system from both the Alerts and Action HTTP APIs
This commit is contained in:
parent
670520a253
commit
4abe864f10
226 changed files with 10844 additions and 1704 deletions
|
@ -4,6 +4,6 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions", "developerExamples"],
|
||||
"requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions", "features", "developerExamples"],
|
||||
"optionalPlugins": []
|
||||
}
|
||||
|
|
|
@ -18,20 +18,56 @@
|
|||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'kibana/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server';
|
||||
|
||||
import { alertType as alwaysFiringAlert } from './alert_types/always_firing';
|
||||
import { alertType as peopleInSpaceAlert } from './alert_types/astros';
|
||||
import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server';
|
||||
import { ALERTING_EXAMPLE_APP_ID } from '../common/constants';
|
||||
|
||||
// this plugin's dependendencies
|
||||
export interface AlertingExampleDeps {
|
||||
alerts: AlertingSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
}
|
||||
|
||||
export class AlertingExamplePlugin implements Plugin<void, void, AlertingExampleDeps> {
|
||||
public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) {
|
||||
public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) {
|
||||
alerts.registerType(alwaysFiringAlert);
|
||||
alerts.registerType(peopleInSpaceAlert);
|
||||
|
||||
features.registerFeature({
|
||||
id: ALERTING_EXAMPLE_APP_ID,
|
||||
name: i18n.translate('alertsExample.featureRegistry.alertsExampleFeatureName', {
|
||||
defaultMessage: 'Alerts Example',
|
||||
}),
|
||||
app: [],
|
||||
alerting: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
alerting: {
|
||||
all: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['alerting:show'],
|
||||
},
|
||||
read: {
|
||||
alerting: {
|
||||
read: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['alerting:show'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "actions"],
|
||||
"requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog"],
|
||||
"optionalPlugins": ["usageCollection", "spaces"],
|
||||
"requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog", "features"],
|
||||
"optionalPlugins": ["usageCollection", "spaces", "security"],
|
||||
"ui": false
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ const createActionsClientMock = () => {
|
|||
getBulk: jest.fn(),
|
||||
execute: jest.fn(),
|
||||
enqueueExecution: jest.fn(),
|
||||
listTypes: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -22,11 +22,14 @@ import {
|
|||
import { actionExecutorMock } from './lib/action_executor.mock';
|
||||
import uuid from 'uuid';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { ActionsAuthorization } from './authorization/actions_authorization';
|
||||
import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
|
||||
|
||||
const defaultKibanaIndex = '.kibana';
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
|
||||
const actionExecutor = actionExecutorMock.create();
|
||||
const authorization = actionsAuthorizationMock.create();
|
||||
const executionEnqueuer = jest.fn();
|
||||
const request = {} as KibanaRequest;
|
||||
|
||||
|
@ -55,17 +58,88 @@ beforeEach(() => {
|
|||
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
preconfiguredActions: [],
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
});
|
||||
});
|
||||
|
||||
describe('create()', () => {
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to create this type of action', async () => {
|
||||
const savedObjectCreateResult = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
|
||||
await actionsClient.create({
|
||||
action: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create this type of action', async () => {
|
||||
const savedObjectCreateResult = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to create a "my-action-type" action`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
actionsClient.create({
|
||||
action: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to create a "my-action-type" action]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
|
||||
});
|
||||
});
|
||||
|
||||
test('creates an action with all given properties', async () => {
|
||||
const savedObjectCreateResult = {
|
||||
id: '1',
|
||||
|
@ -83,7 +157,7 @@ describe('create()', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
const result = await actionsClient.create({
|
||||
action: {
|
||||
name: 'my name',
|
||||
|
@ -99,8 +173,8 @@ describe('create()', () => {
|
|||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
});
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
Object {
|
||||
|
@ -161,7 +235,7 @@ describe('create()', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
|
@ -199,8 +273,8 @@ describe('create()', () => {
|
|||
c: true,
|
||||
},
|
||||
});
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
Object {
|
||||
|
@ -237,13 +311,14 @@ describe('create()', () => {
|
|||
actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams);
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
preconfiguredActions: [],
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
});
|
||||
|
||||
const savedObjectCreateResult = {
|
||||
|
@ -262,7 +337,7 @@ describe('create()', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
|
||||
await expect(
|
||||
actionsClient.create({
|
||||
|
@ -298,7 +373,7 @@ describe('create()', () => {
|
|||
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
|
||||
throw new Error('Fail');
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
await expect(
|
||||
actionsClient.create({
|
||||
action: {
|
||||
|
@ -313,8 +388,118 @@ describe('create()', () => {
|
|||
});
|
||||
|
||||
describe('get()', () => {
|
||||
test('calls savedObjectsClient with id', async () => {
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await actionsClient.get({ id: '1' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('ensures user is authorised to get preconfigured type of action', async () => {
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: 'my-action-type',
|
||||
secrets: {
|
||||
test: 'test1',
|
||||
},
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionsClient.get({ id: 'testPreconfigured' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "my-action-type" action`)
|
||||
);
|
||||
|
||||
await expect(actionsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "my-action-type" action]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create preconfigured of action', async () => {
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: 'my-action-type',
|
||||
secrets: {
|
||||
test: 'test1',
|
||||
},
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "my-action-type" action`)
|
||||
);
|
||||
|
||||
await expect(actionsClient.get({ id: 'testPreconfigured' })).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "my-action-type" action]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls unsecuredSavedObjectsClient with id', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {},
|
||||
|
@ -325,8 +510,8 @@ describe('get()', () => {
|
|||
id: '1',
|
||||
isPreconfigured: false,
|
||||
});
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"1",
|
||||
|
@ -337,12 +522,13 @@ describe('get()', () => {
|
|||
test('return predefined action with id', async () => {
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
|
@ -366,12 +552,84 @@ describe('get()', () => {
|
|||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
});
|
||||
expect(savedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll()', () => {
|
||||
test('calls savedObjectsClient with parameters', async () => {
|
||||
describe('authorization', () => {
|
||||
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
},
|
||||
});
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return actionsClient.getAll();
|
||||
}
|
||||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getAllOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get all actions`)
|
||||
);
|
||||
|
||||
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls unsecuredSavedObjectsClient with parameters', async () => {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
|
@ -391,7 +649,7 @@ describe('getAll()', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
|
@ -401,12 +659,13 @@ describe('getAll()', () => {
|
|||
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
|
@ -443,8 +702,76 @@ describe('getAll()', () => {
|
|||
});
|
||||
|
||||
describe('getBulk()', () => {
|
||||
test('calls getBulk savedObjectsClient with parameters', async () => {
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
describe('authorization', () => {
|
||||
function getBulkOperation(): ReturnType<ActionsClient['getBulk']> {
|
||||
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
},
|
||||
});
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return actionsClient.getBulk(['1', 'testPreconfigured']);
|
||||
}
|
||||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getBulkOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get all actions`)
|
||||
);
|
||||
|
||||
await expect(getBulkOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => {
|
||||
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -469,12 +796,13 @@ describe('getBulk()', () => {
|
|||
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization: (authorization as unknown) as ActionsAuthorization,
|
||||
preconfiguredActions: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
|
@ -514,13 +842,32 @@ describe('getBulk()', () => {
|
|||
});
|
||||
|
||||
describe('delete()', () => {
|
||||
test('calls savedObjectsClient with id', async () => {
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to delete actions', async () => {
|
||||
await actionsClient.delete({ id: '1' });
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to delete all actions`)
|
||||
);
|
||||
|
||||
await expect(actionsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to delete all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls unsecuredSavedObjectsClient with id', async () => {
|
||||
const expectedResult = Symbol();
|
||||
savedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
|
||||
unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
|
||||
const result = await actionsClient.delete({ id: '1' });
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"1",
|
||||
|
@ -530,6 +877,60 @@ describe('delete()', () => {
|
|||
});
|
||||
|
||||
describe('update()', () => {
|
||||
describe('authorization', () => {
|
||||
function updateOperation(): ReturnType<ActionsClient['update']> {
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'my-action-type',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'my-action-type',
|
||||
name: 'my name',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
return actionsClient.update({
|
||||
id: 'my-action',
|
||||
action: {
|
||||
name: 'my name',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
test('ensures user is authorised to update actions', async () => {
|
||||
await updateOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to update all actions`)
|
||||
);
|
||||
|
||||
await expect(updateOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to update all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
|
||||
});
|
||||
});
|
||||
|
||||
test('updates an action with all given properties', async () => {
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
|
@ -537,7 +938,7 @@ describe('update()', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -545,7 +946,7 @@ describe('update()', () => {
|
|||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -571,8 +972,8 @@ describe('update()', () => {
|
|||
name: 'my name',
|
||||
config: {},
|
||||
});
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"my-action",
|
||||
|
@ -584,8 +985,8 @@ describe('update()', () => {
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"my-action",
|
||||
|
@ -605,7 +1006,7 @@ describe('update()', () => {
|
|||
},
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -634,7 +1035,7 @@ describe('update()', () => {
|
|||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -642,7 +1043,7 @@ describe('update()', () => {
|
|||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -680,8 +1081,8 @@ describe('update()', () => {
|
|||
c: true,
|
||||
},
|
||||
});
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"my-action",
|
||||
|
@ -709,7 +1110,7 @@ describe('update()', () => {
|
|||
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
|
||||
throw new Error('Fail');
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -717,7 +1118,7 @@ describe('update()', () => {
|
|||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: 'my-action',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
|
@ -742,6 +1143,35 @@ describe('update()', () => {
|
|||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to excecute actions', async () => {
|
||||
await actionsClient.execute({
|
||||
actionId: 'action-id',
|
||||
params: {
|
||||
name: 'my name',
|
||||
},
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to execute all actions`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
actionsClient.execute({
|
||||
actionId: 'action-id',
|
||||
params: {
|
||||
name: 'my name',
|
||||
},
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the actionExecutor with the appropriate parameters', async () => {
|
||||
const actionId = uuid.v4();
|
||||
actionExecutor.execute.mockResolvedValue({ status: 'ok', actionId });
|
||||
|
@ -765,6 +1195,35 @@ describe('execute()', () => {
|
|||
});
|
||||
|
||||
describe('enqueueExecution()', () => {
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to excecute actions', async () => {
|
||||
await actionsClient.enqueueExecution({
|
||||
id: uuid.v4(),
|
||||
params: {},
|
||||
spaceId: 'default',
|
||||
apiKey: null,
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to execute all actions`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
actionsClient.enqueueExecution({
|
||||
id: uuid.v4(),
|
||||
params: {},
|
||||
spaceId: 'default',
|
||||
apiKey: null,
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the executionEnqueuer with the appropriate parameters', async () => {
|
||||
const opts = {
|
||||
id: uuid.v4(),
|
||||
|
@ -774,6 +1233,6 @@ describe('enqueueExecution()', () => {
|
|||
};
|
||||
await expect(actionsClient.enqueueExecution(opts)).resolves.toMatchInlineSnapshot(`undefined`);
|
||||
|
||||
expect(executionEnqueuer).toHaveBeenCalledWith(savedObjectsClient, opts);
|
||||
expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
ExecutionEnqueuer,
|
||||
ExecuteOptions as EnqueueExecutionOptions,
|
||||
} from './create_execute_function';
|
||||
import { ActionsAuthorization } from './authorization/actions_authorization';
|
||||
import { ActionType } from '../common';
|
||||
|
||||
// We are assuming there won't be many actions. This is why we will load
|
||||
// all the actions in advance and assume the total count to not go over 10000.
|
||||
|
@ -52,11 +54,12 @@ interface ConstructorOptions {
|
|||
defaultKibanaIndex: string;
|
||||
scopedClusterClient: ILegacyScopedClusterClient;
|
||||
actionTypeRegistry: ActionTypeRegistry;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
preconfiguredActions: PreConfiguredAction[];
|
||||
actionExecutor: ActionExecutorContract;
|
||||
executionEnqueuer: ExecutionEnqueuer;
|
||||
request: KibanaRequest;
|
||||
authorization: ActionsAuthorization;
|
||||
}
|
||||
|
||||
interface UpdateOptions {
|
||||
|
@ -67,45 +70,51 @@ interface UpdateOptions {
|
|||
export class ActionsClient {
|
||||
private readonly defaultKibanaIndex: string;
|
||||
private readonly scopedClusterClient: ILegacyScopedClusterClient;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly actionTypeRegistry: ActionTypeRegistry;
|
||||
private readonly preconfiguredActions: PreConfiguredAction[];
|
||||
private readonly actionExecutor: ActionExecutorContract;
|
||||
private readonly request: KibanaRequest;
|
||||
private readonly authorization: ActionsAuthorization;
|
||||
private readonly executionEnqueuer: ExecutionEnqueuer;
|
||||
|
||||
constructor({
|
||||
actionTypeRegistry,
|
||||
defaultKibanaIndex,
|
||||
scopedClusterClient,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
preconfiguredActions,
|
||||
actionExecutor,
|
||||
executionEnqueuer,
|
||||
request,
|
||||
authorization,
|
||||
}: ConstructorOptions) {
|
||||
this.actionTypeRegistry = actionTypeRegistry;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
|
||||
this.scopedClusterClient = scopedClusterClient;
|
||||
this.defaultKibanaIndex = defaultKibanaIndex;
|
||||
this.preconfiguredActions = preconfiguredActions;
|
||||
this.actionExecutor = actionExecutor;
|
||||
this.executionEnqueuer = executionEnqueuer;
|
||||
this.request = request;
|
||||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action
|
||||
*/
|
||||
public async create({ action }: CreateOptions): Promise<ActionResult> {
|
||||
const { actionTypeId, name, config, secrets } = action;
|
||||
public async create({
|
||||
action: { actionTypeId, name, config, secrets },
|
||||
}: CreateOptions): Promise<ActionResult> {
|
||||
await this.authorization.ensureAuthorized('create', actionTypeId);
|
||||
|
||||
const actionType = this.actionTypeRegistry.get(actionTypeId);
|
||||
const validatedActionTypeConfig = validateConfig(actionType, config);
|
||||
const validatedActionTypeSecrets = validateSecrets(actionType, secrets);
|
||||
|
||||
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
|
||||
const result = await this.savedObjectsClient.create('action', {
|
||||
const result = await this.unsecuredSavedObjectsClient.create('action', {
|
||||
actionTypeId,
|
||||
name,
|
||||
config: validatedActionTypeConfig as SavedObjectAttributes,
|
||||
|
@ -125,6 +134,8 @@ export class ActionsClient {
|
|||
* Update action
|
||||
*/
|
||||
public async update({ id, action }: UpdateOptions): Promise<ActionResult> {
|
||||
await this.authorization.ensureAuthorized('update');
|
||||
|
||||
if (
|
||||
this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
|
||||
undefined
|
||||
|
@ -139,7 +150,7 @@ export class ActionsClient {
|
|||
'update'
|
||||
);
|
||||
}
|
||||
const existingObject = await this.savedObjectsClient.get<RawAction>('action', id);
|
||||
const existingObject = await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
|
||||
const { actionTypeId } = existingObject.attributes;
|
||||
const { name, config, secrets } = action;
|
||||
const actionType = this.actionTypeRegistry.get(actionTypeId);
|
||||
|
@ -148,7 +159,7 @@ export class ActionsClient {
|
|||
|
||||
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
|
||||
const result = await this.savedObjectsClient.update<RawAction>('action', id, {
|
||||
const result = await this.unsecuredSavedObjectsClient.update<RawAction>('action', id, {
|
||||
actionTypeId,
|
||||
name,
|
||||
config: validatedActionTypeConfig as SavedObjectAttributes,
|
||||
|
@ -168,6 +179,8 @@ export class ActionsClient {
|
|||
* Get an action
|
||||
*/
|
||||
public async get({ id }: { id: string }): Promise<ActionResult> {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
|
||||
const preconfiguredActionsList = this.preconfiguredActions.find(
|
||||
(preconfiguredAction) => preconfiguredAction.id === id
|
||||
);
|
||||
|
@ -179,7 +192,7 @@ export class ActionsClient {
|
|||
isPreconfigured: true,
|
||||
};
|
||||
}
|
||||
const result = await this.savedObjectsClient.get<RawAction>('action', id);
|
||||
const result = await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
@ -194,8 +207,10 @@ export class ActionsClient {
|
|||
* Get all actions with preconfigured list
|
||||
*/
|
||||
public async getAll(): Promise<FindActionResult[]> {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
|
||||
const savedObjectsActions = (
|
||||
await this.savedObjectsClient.find<RawAction>({
|
||||
await this.unsecuredSavedObjectsClient.find<RawAction>({
|
||||
perPage: MAX_ACTIONS_RETURNED,
|
||||
type: 'action',
|
||||
})
|
||||
|
@ -221,6 +236,8 @@ export class ActionsClient {
|
|||
* Get bulk actions with preconfigured list
|
||||
*/
|
||||
public async getBulk(ids: string[]): Promise<ActionResult[]> {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
|
||||
const actionResults = new Array<ActionResult>();
|
||||
for (const actionId of ids) {
|
||||
const action = this.preconfiguredActions.find(
|
||||
|
@ -242,7 +259,7 @@ export class ActionsClient {
|
|||
];
|
||||
|
||||
const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' }));
|
||||
const bulkGetResult = await this.savedObjectsClient.bulkGet<RawAction>(bulkGetOpts);
|
||||
const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet<RawAction>(bulkGetOpts);
|
||||
|
||||
for (const action of bulkGetResult.saved_objects) {
|
||||
if (action.error) {
|
||||
|
@ -259,6 +276,8 @@ export class ActionsClient {
|
|||
* Delete action
|
||||
*/
|
||||
public async delete({ id }: { id: string }) {
|
||||
await this.authorization.ensureAuthorized('delete');
|
||||
|
||||
if (
|
||||
this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
|
||||
undefined
|
||||
|
@ -273,18 +292,24 @@ export class ActionsClient {
|
|||
'delete'
|
||||
);
|
||||
}
|
||||
return await this.savedObjectsClient.delete('action', id);
|
||||
return await this.unsecuredSavedObjectsClient.delete('action', id);
|
||||
}
|
||||
|
||||
public async execute({
|
||||
actionId,
|
||||
params,
|
||||
}: Omit<ExecuteOptions, 'request'>): Promise<ActionTypeExecutorResult> {
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
return this.actionExecutor.execute({ actionId, params, request: this.request });
|
||||
}
|
||||
|
||||
public async enqueueExecution(options: EnqueueExecutionOptions): Promise<void> {
|
||||
return this.executionEnqueuer(this.savedObjectsClient, options);
|
||||
await this.authorization.ensureAuthorized('execute');
|
||||
return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options);
|
||||
}
|
||||
|
||||
public async listTypes(): Promise<ActionType[]> {
|
||||
return this.actionTypeRegistry.list();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionsAuthorization } from './actions_authorization';
|
||||
|
||||
export type ActionsAuthorizationMock = jest.Mocked<PublicMethodsOf<ActionsAuthorization>>;
|
||||
|
||||
const createActionsAuthorizationMock = () => {
|
||||
const mocked: ActionsAuthorizationMock = {
|
||||
ensureAuthorized: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const actionsAuthorizationMock: {
|
||||
create: () => ActionsAuthorizationMock;
|
||||
} = {
|
||||
create: createActionsAuthorizationMock,
|
||||
};
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { securityMock } from '../../../../plugins/security/server/mocks';
|
||||
import { ActionsAuthorization } from './actions_authorization';
|
||||
import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock';
|
||||
import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger';
|
||||
import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
|
||||
const request = {} as KibanaRequest;
|
||||
|
||||
const auditLogger = actionsAuthorizationAuditLoggerMock.create();
|
||||
const realAuditLogger = new ActionsAuthorizationAuditLogger();
|
||||
|
||||
const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`;
|
||||
function mockSecurity() {
|
||||
const security = securityMock.createSetup();
|
||||
const authorization = security.authz;
|
||||
// typescript is having trouble inferring jest's automocking
|
||||
(authorization.actions.savedObject.get as jest.MockedFunction<
|
||||
typeof authorization.actions.savedObject.get
|
||||
>).mockImplementation(mockAuthorizationAction);
|
||||
authorization.mode.useRbacForRequest.mockReturnValue(true);
|
||||
return { authorization };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
auditLogger.actionsAuthorizationFailure.mockImplementation((username, ...args) =>
|
||||
realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args)
|
||||
);
|
||||
auditLogger.actionsAuthorizationSuccess.mockImplementation((username, ...args) =>
|
||||
realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args)
|
||||
);
|
||||
});
|
||||
|
||||
describe('ensureAuthorized', () => {
|
||||
test('is a no-op when there is no authorization api', async () => {
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
});
|
||||
|
||||
test('is a no-op when the security license is disabled', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
authorization.mode.useRbacForRequest.mockReturnValue(false);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
});
|
||||
|
||||
test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<ReturnType<
|
||||
typeof authorization.checkPrivilegesDynamicallyWithRequest
|
||||
>> = jest.fn();
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: true,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'create'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('create', 'myType');
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
|
||||
expect(checkPrivileges).toHaveBeenCalledWith(mockAuthorizationAction('action', 'create'));
|
||||
|
||||
expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"some-user",
|
||||
"create",
|
||||
"myType",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('ensures the user has privileges to execute an Actions Saved Object type', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<ReturnType<
|
||||
typeof authorization.checkPrivilegesDynamicallyWithRequest
|
||||
>> = jest.fn();
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: true,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'execute'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized('execute', 'myType');
|
||||
|
||||
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([
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
]);
|
||||
|
||||
expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"some-user",
|
||||
"execute",
|
||||
"myType",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws if user lacks the required privieleges', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<ReturnType<
|
||||
typeof authorization.checkPrivilegesDynamicallyWithRequest
|
||||
>> = jest.fn();
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: false,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'create'),
|
||||
authorized: false,
|
||||
},
|
||||
{
|
||||
privilege: mockAuthorizationAction('myOtherType', 'create'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
actionsAuthorization.ensureAuthorized('create', 'myType')
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized to create a \\"myType\\" action"`);
|
||||
|
||||
expect(auditLogger.actionsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
expect(auditLogger.actionsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.actionsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"some-user",
|
||||
"create",
|
||||
"myType",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { ActionsAuthorizationAuditLogger } from './audit_logger';
|
||||
import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
|
||||
export interface ConstructorOptions {
|
||||
request: KibanaRequest;
|
||||
auditLogger: ActionsAuthorizationAuditLogger;
|
||||
authorization?: SecurityPluginSetup['authz'];
|
||||
}
|
||||
|
||||
const operationAlias: Record<
|
||||
string,
|
||||
(authorization: SecurityPluginSetup['authz']) => string | 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'),
|
||||
};
|
||||
|
||||
export class ActionsAuthorization {
|
||||
private readonly request: KibanaRequest;
|
||||
private readonly authorization?: SecurityPluginSetup['authz'];
|
||||
private readonly auditLogger: ActionsAuthorizationAuditLogger;
|
||||
|
||||
constructor({ request, authorization, auditLogger }: ConstructorOptions) {
|
||||
this.request = request;
|
||||
this.authorization = authorization;
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
public async ensureAuthorized(operation: string, actionTypeId?: string) {
|
||||
const { authorization } = this;
|
||||
if (authorization?.mode?.useRbacForRequest(this.request)) {
|
||||
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
|
||||
const { hasAllRequested, username } = await checkPrivileges(
|
||||
operationAlias[operation]
|
||||
? operationAlias[operation](authorization)
|
||||
: authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)
|
||||
);
|
||||
if (hasAllRequested) {
|
||||
this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
|
||||
} else {
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionsAuthorizationAuditLogger } from './audit_logger';
|
||||
|
||||
const createActionsAuthorizationAuditLoggerMock = () => {
|
||||
const mocked = ({
|
||||
getAuthorizationMessage: jest.fn(),
|
||||
actionsAuthorizationFailure: jest.fn(),
|
||||
actionsAuthorizationSuccess: jest.fn(),
|
||||
} as unknown) as jest.Mocked<ActionsAuthorizationAuditLogger>;
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const actionsAuthorizationAuditLoggerMock: {
|
||||
create: () => jest.Mocked<ActionsAuthorizationAuditLogger>;
|
||||
} = {
|
||||
create: createActionsAuthorizationAuditLoggerMock,
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ActionsAuthorizationAuditLogger } from './audit_logger';
|
||||
|
||||
const createMockAuditLogger = () => {
|
||||
return {
|
||||
log: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
describe(`#constructor`, () => {
|
||||
test('initializes a noop auditLogger if security logger is unavailable', () => {
|
||||
const actionsAuditLogger = new ActionsAuthorizationAuditLogger(undefined);
|
||||
|
||||
const username = 'foo-user';
|
||||
const actionTypeId = 'action-type-id';
|
||||
const operation = 'create';
|
||||
expect(() => {
|
||||
actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId);
|
||||
actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#actionsAuthorizationFailure`, () => {
|
||||
test('logs auth failure', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const actionTypeId = 'action-type-id';
|
||||
const operation = 'create';
|
||||
|
||||
actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"actions_authorization_failure",
|
||||
"foo-user Unauthorized to create a \\"action-type-id\\" action",
|
||||
Object {
|
||||
"actionTypeId": "action-type-id",
|
||||
"operation": "create",
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#savedObjectsAuthorizationSuccess`, () => {
|
||||
test('logs auth success', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const actionTypeId = 'action-type-id';
|
||||
|
||||
const operation = 'create';
|
||||
|
||||
actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"actions_authorization_success",
|
||||
"foo-user Authorized to create a \\"action-type-id\\" action",
|
||||
Object {
|
||||
"actionTypeId": "action-type-id",
|
||||
"operation": "create",
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
66
x-pack/plugins/actions/server/authorization/audit_logger.ts
Normal file
66
x-pack/plugins/actions/server/authorization/audit_logger.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
|
||||
export enum AuthorizationResult {
|
||||
Unauthorized = 'Unauthorized',
|
||||
Authorized = 'Authorized',
|
||||
}
|
||||
|
||||
export class ActionsAuthorizationAuditLogger {
|
||||
private readonly auditLogger: AuditLogger;
|
||||
|
||||
constructor(auditLogger: AuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
public getAuthorizationMessage(
|
||||
authorizationResult: AuthorizationResult,
|
||||
operation: string,
|
||||
actionTypeId?: string
|
||||
): string {
|
||||
return `${authorizationResult} to ${operation} ${
|
||||
actionTypeId ? `a "${actionTypeId}" action` : `actions`
|
||||
}`;
|
||||
}
|
||||
|
||||
public actionsAuthorizationFailure(
|
||||
username: string,
|
||||
operation: string,
|
||||
actionTypeId?: string
|
||||
): string {
|
||||
const message = this.getAuthorizationMessage(
|
||||
AuthorizationResult.Unauthorized,
|
||||
operation,
|
||||
actionTypeId
|
||||
);
|
||||
this.auditLogger.log('actions_authorization_failure', `${username} ${message}`, {
|
||||
username,
|
||||
actionTypeId,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
public actionsAuthorizationSuccess(
|
||||
username: string,
|
||||
operation: string,
|
||||
actionTypeId?: string
|
||||
): string {
|
||||
const message = this.getAuthorizationMessage(
|
||||
AuthorizationResult.Authorized,
|
||||
operation,
|
||||
actionTypeId
|
||||
);
|
||||
this.auditLogger.log('actions_authorization_success', `${username} ${message}`, {
|
||||
username,
|
||||
actionTypeId,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
import { SavedObjectsClientContract } from '../../../../src/core/server';
|
||||
import { TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './types';
|
||||
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
|
||||
|
||||
interface CreateExecuteFunctionOptions {
|
||||
taskManager: TaskManagerStartContract;
|
||||
|
@ -49,11 +50,14 @@ export function createExecutionEnqueuerFunction({
|
|||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
}
|
||||
|
||||
const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
|
||||
actionId: id,
|
||||
params,
|
||||
apiKey,
|
||||
});
|
||||
const actionTaskParamsRecord = await savedObjectsClient.create(
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
actionId: id,
|
||||
params,
|
||||
apiKey,
|
||||
}
|
||||
);
|
||||
|
||||
await taskManager.schedule({
|
||||
taskType: `actions:${actionTypeId}`,
|
||||
|
|
41
x-pack/plugins/actions/server/feature.ts
Normal file
41
x-pack/plugins/actions/server/feature.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
|
||||
|
||||
export const ACTIONS_FEATURE = {
|
||||
id: 'actions',
|
||||
name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
icon: 'bell',
|
||||
navLinkId: 'actions',
|
||||
app: [],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [],
|
||||
api: [],
|
||||
catalogue: [],
|
||||
savedObject: {
|
||||
all: [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show', 'execute', 'save', 'delete'],
|
||||
},
|
||||
read: {
|
||||
app: [],
|
||||
api: [],
|
||||
catalogue: [],
|
||||
savedObject: {
|
||||
// action execution requires 'read' over `actions`, but 'all' over `action_task_params`
|
||||
all: [ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],
|
||||
read: [ACTION_SAVED_OBJECT_TYPE],
|
||||
},
|
||||
ui: ['show', 'execute'],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -8,8 +8,10 @@ import { PluginInitializerContext } from '../../../../src/core/server';
|
|||
import { ActionsPlugin } from './plugin';
|
||||
import { configSchema } from './config';
|
||||
import { ActionsClient as ActionsClientClass } from './actions_client';
|
||||
import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization';
|
||||
|
||||
export type ActionsClient = PublicMethodsOf<ActionsClientClass>;
|
||||
export type ActionsAuthorization = PublicMethodsOf<ActionsAuthorizationClass>;
|
||||
|
||||
export {
|
||||
ActionsPlugin,
|
||||
|
|
|
@ -9,15 +9,17 @@ import { schema } from '@kbn/config-schema';
|
|||
import { ActionExecutor } from './action_executor';
|
||||
import { actionTypeRegistryMock } from '../action_type_registry.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
|
||||
import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/mocks';
|
||||
import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock';
|
||||
import { ActionType } from '../types';
|
||||
import { actionsMock } from '../mocks';
|
||||
import { actionsMock, actionsClientMock } from '../mocks';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false });
|
||||
const services = actionsMock.createServices();
|
||||
const savedObjectsClientWithHidden = savedObjectsClientMock.create();
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
||||
|
@ -30,11 +32,12 @@ const executeParams = {
|
|||
};
|
||||
|
||||
const spacesMock = spacesServiceMock.createSetupContract();
|
||||
const getActionsClientWithRequest = jest.fn();
|
||||
actionExecutor.initialize({
|
||||
logger: loggingSystemMock.create().get(),
|
||||
spaces: spacesMock,
|
||||
getServices: () => services,
|
||||
getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
|
||||
getActionsClientWithRequest,
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsClient,
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
|
@ -44,6 +47,7 @@ actionExecutor.initialize({
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
spacesMock.getSpaceId.mockReturnValue('some-namespace');
|
||||
getActionsClientWithRequest.mockResolvedValue(actionsClient);
|
||||
});
|
||||
|
||||
test('successfully executes', async () => {
|
||||
|
@ -67,7 +71,13 @@ test('successfully executes', async () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
...pick(actionSavedObject.attributes, 'actionTypeId', 'config'),
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
await actionExecutor.execute(executeParams);
|
||||
|
@ -108,7 +118,13 @@ test('provides empty config when config and / or secrets is empty', async () =>
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
actionTypeId: actionSavedObject.attributes.actionTypeId,
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
await actionExecutor.execute(executeParams);
|
||||
|
@ -138,7 +154,13 @@ test('throws an error when config is invalid', async () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
actionTypeId: actionSavedObject.attributes.actionTypeId,
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
|
||||
|
@ -171,7 +193,13 @@ test('throws an error when params is invalid', async () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
actionTypeId: actionSavedObject.attributes.actionTypeId,
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
|
||||
|
@ -185,7 +213,7 @@ test('throws an error when params is invalid', async () => {
|
|||
});
|
||||
|
||||
test('throws an error when failing to load action through savedObjectsClient', async () => {
|
||||
savedObjectsClientWithHidden.get.mockRejectedValueOnce(new Error('No access'));
|
||||
actionsClient.get.mockRejectedValueOnce(new Error('No access'));
|
||||
await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"No access"`
|
||||
);
|
||||
|
@ -206,7 +234,13 @@ test('throws an error if actionType is not enabled', async () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
actionTypeId: actionSavedObject.attributes.actionTypeId,
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
|
||||
|
@ -240,7 +274,13 @@ test('should not throws an error if actionType is preconfigured', async () => {
|
|||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
|
||||
const actionResult = {
|
||||
id: actionSavedObject.id,
|
||||
name: actionSavedObject.id,
|
||||
...pick(actionSavedObject.attributes, 'actionTypeId', 'config', 'secrets'),
|
||||
isPreconfigured: false,
|
||||
};
|
||||
actionsClient.get.mockResolvedValueOnce(actionResult);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
|
||||
|
@ -268,7 +308,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
|
|||
customActionExecutor.initialize({
|
||||
logger: loggingSystemMock.create().get(),
|
||||
spaces: spacesMock,
|
||||
getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
|
||||
getActionsClientWithRequest,
|
||||
getServices: () => services,
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsClient,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger, KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server';
|
||||
import { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
|
||||
import {
|
||||
ActionTypeExecutorResult,
|
||||
|
@ -15,14 +15,15 @@ import {
|
|||
} from '../types';
|
||||
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
|
||||
import { SpacesServiceSetup } from '../../../spaces/server';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { EVENT_LOG_ACTIONS, PluginStartContract } from '../plugin';
|
||||
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { ActionsClient } from '../actions_client';
|
||||
|
||||
export interface ActionExecutorContext {
|
||||
logger: Logger;
|
||||
spaces?: SpacesServiceSetup;
|
||||
getServices: GetServicesFunction;
|
||||
getScopedSavedObjectsClient: (req: KibanaRequest) => SavedObjectsClientContract;
|
||||
getActionsClientWithRequest: PluginStartContract['getActionsClientWithRequest'];
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
eventLogger: IEventLogger;
|
||||
|
@ -76,7 +77,7 @@ export class ActionExecutor {
|
|||
actionTypeRegistry,
|
||||
eventLogger,
|
||||
preconfiguredActions,
|
||||
getScopedSavedObjectsClient,
|
||||
getActionsClientWithRequest,
|
||||
} = this.actionExecutorContext!;
|
||||
|
||||
const services = getServices(request);
|
||||
|
@ -84,7 +85,7 @@ export class ActionExecutor {
|
|||
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};
|
||||
|
||||
const { actionTypeId, name, config, secrets } = await getActionInfo(
|
||||
getScopedSavedObjectsClient(request),
|
||||
await getActionsClientWithRequest(request),
|
||||
encryptedSavedObjectsClient,
|
||||
preconfiguredActions,
|
||||
actionId,
|
||||
|
@ -196,7 +197,7 @@ interface ActionInfo {
|
|||
}
|
||||
|
||||
async function getActionInfo(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
actionsClient: PublicMethodsOf<ActionsClient>,
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
|
||||
preconfiguredActions: PreConfiguredAction[],
|
||||
actionId: string,
|
||||
|
@ -217,9 +218,7 @@ async function getActionInfo(
|
|||
|
||||
// if not pre-configured action, should be a saved object
|
||||
// ensure user can read the action before processing
|
||||
const {
|
||||
attributes: { actionTypeId, config, name },
|
||||
} = await savedObjectsClient.get<RawAction>('action', actionId);
|
||||
const { actionTypeId, config, name } = await actionsClient.get({ id: actionId });
|
||||
|
||||
const {
|
||||
attributes: { secrets },
|
||||
|
|
|
@ -15,6 +15,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
|
|||
import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/mocks';
|
||||
import { ActionTypeDisabledError } from './errors';
|
||||
import { actionsClientMock } from '../mocks';
|
||||
|
||||
const spaceIdToNamespace = jest.fn();
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
@ -59,7 +60,7 @@ const actionExecutorInitializerParams = {
|
|||
logger: loggingSystemMock.create().get(),
|
||||
getServices: jest.fn().mockReturnValue(services),
|
||||
actionTypeRegistry,
|
||||
getScopedSavedObjectsClient: () => savedObjectsClientMock.create(),
|
||||
getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()),
|
||||
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
|
||||
eventLogger: eventLoggerMock.create(),
|
||||
preconfiguredActions: [],
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
SpaceIdToNamespaceFunction,
|
||||
ActionTypeExecutorResult,
|
||||
} from '../types';
|
||||
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
|
||||
|
||||
export interface TaskRunnerContext {
|
||||
logger: Logger;
|
||||
|
@ -66,7 +67,7 @@ export class TaskRunnerFactory {
|
|||
const {
|
||||
attributes: { actionId, params, apiKey },
|
||||
} = await encryptedSavedObjectsClient.getDecryptedAsInternalUser<ActionTaskParams>(
|
||||
'action_task_params',
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
actionTaskParamsId,
|
||||
{ namespace }
|
||||
);
|
||||
|
@ -121,11 +122,11 @@ export class TaskRunnerFactory {
|
|||
// Cleanup action_task_params object now that we're done with it
|
||||
try {
|
||||
const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest);
|
||||
await savedObjectsClient.delete('action_task_params', actionTaskParamsId);
|
||||
await savedObjectsClient.delete(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId);
|
||||
} catch (e) {
|
||||
// Log error only, we shouldn't fail the task because of an error here (if ever there's retry logic)
|
||||
logger.error(
|
||||
`Failed to cleanup action_task_params object [id="${actionTaskParamsId}"]: ${e.message}`
|
||||
`Failed to cleanup ${ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE} object [id="${actionTaskParamsId}"]: ${e.message}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
elasticsearchServiceMock,
|
||||
savedObjectsClientMock,
|
||||
} from '../../../../src/core/server/mocks';
|
||||
import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
|
||||
|
||||
export { actionsAuthorizationMock };
|
||||
export { actionsClientMock };
|
||||
|
||||
const createSetupMock = () => {
|
||||
|
@ -26,6 +28,9 @@ const createStartMock = () => {
|
|||
isActionTypeEnabled: jest.fn(),
|
||||
isActionExecutable: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
|
||||
getActionsAuthorizationWithRequest: jest
|
||||
.fn()
|
||||
.mockReturnValue(actionsAuthorizationMock.create()),
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
return mock;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { PluginInitializerContext, RequestHandlerContext } from '../../../../src
|
|||
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks';
|
||||
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
|
||||
import { licensingMock } from '../../licensing/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
|
||||
import { taskManagerMock } from '../../task_manager/server/mocks';
|
||||
import { eventLogMock } from '../../event_log/server/mocks';
|
||||
|
@ -43,6 +44,7 @@ describe('Actions Plugin', () => {
|
|||
licensing: licensingMock.createSetup(),
|
||||
eventLog: eventLogMock.createSetup(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
features: featuresPluginMock.createSetup(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -200,6 +202,7 @@ describe('Actions Plugin', () => {
|
|||
licensing: licensingMock.createSetup(),
|
||||
eventLog: eventLogMock.createSetup(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
features: featuresPluginMock.createSetup(),
|
||||
};
|
||||
pluginsStart = {
|
||||
taskManager: taskManagerMock.createStart(),
|
||||
|
|
|
@ -29,6 +29,8 @@ import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_m
|
|||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { LICENSE_TYPE } from '../../licensing/common/types';
|
||||
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
|
||||
import { ActionsConfig } from './config';
|
||||
import { Services, ActionType, PreConfiguredAction } from './types';
|
||||
|
@ -52,7 +54,14 @@ import {
|
|||
} from './routes';
|
||||
import { IEventLogger, IEventLogService } from '../../event_log/server';
|
||||
import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
import {
|
||||
setupSavedObjects,
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
} from './saved_objects';
|
||||
import { ACTIONS_FEATURE } from './feature';
|
||||
import { ActionsAuthorization } from './authorization/actions_authorization';
|
||||
import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger';
|
||||
|
||||
const EVENT_LOG_PROVIDER = 'actions';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -68,6 +77,7 @@ export interface PluginStartContract {
|
|||
isActionTypeEnabled(id: string): boolean;
|
||||
isActionExecutable(actionId: string, actionTypeId: string): boolean;
|
||||
getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>;
|
||||
getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf<ActionsAuthorization>;
|
||||
preconfiguredActions: PreConfiguredAction[];
|
||||
}
|
||||
|
||||
|
@ -78,13 +88,15 @@ export interface ActionsPluginsSetup {
|
|||
spaces?: SpacesPluginSetup;
|
||||
eventLog: IEventLogService;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
}
|
||||
export interface ActionsPluginsStart {
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
}
|
||||
|
||||
const includedHiddenTypes = ['action', 'action_task_params'];
|
||||
const includedHiddenTypes = [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE];
|
||||
|
||||
export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, PluginStartContract> {
|
||||
private readonly kibanaIndex: Promise<string>;
|
||||
|
@ -97,6 +109,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
private actionExecutor?: ActionExecutor;
|
||||
private licenseState: ILicenseState | null = null;
|
||||
private spaces?: SpacesServiceSetup;
|
||||
private security?: SecurityPluginSetup;
|
||||
private eventLogger?: IEventLogger;
|
||||
private isESOUsingEphemeralEncryptionKey?: boolean;
|
||||
private readonly telemetryLogger: Logger;
|
||||
|
@ -131,6 +144,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
);
|
||||
}
|
||||
|
||||
plugins.features.registerFeature(ACTIONS_FEATURE);
|
||||
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
|
||||
|
||||
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
|
||||
|
@ -167,6 +181,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
this.serverBasePath = core.http.basePath.serverBasePath;
|
||||
this.actionExecutor = actionExecutor;
|
||||
this.spaces = plugins.spaces?.spacesService;
|
||||
this.security = plugins.security;
|
||||
|
||||
registerBuiltInActionTypes({
|
||||
logger: this.logger,
|
||||
|
@ -227,16 +242,39 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
kibanaIndex,
|
||||
isESOUsingEphemeralEncryptionKey,
|
||||
preconfiguredActions,
|
||||
instantiateAuthorization,
|
||||
} = this;
|
||||
|
||||
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({
|
||||
includedHiddenTypes,
|
||||
});
|
||||
|
||||
const getScopedSavedObjectsClient = (request: KibanaRequest) =>
|
||||
core.savedObjects.getScopedClient(request, {
|
||||
includedHiddenTypes,
|
||||
const getActionsClientWithRequest = async (request: KibanaRequest) => {
|
||||
if (isESOUsingEphemeralEncryptionKey === true) {
|
||||
throw new Error(
|
||||
`Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
|
||||
);
|
||||
}
|
||||
return new ActionsClient({
|
||||
unsecuredSavedObjectsClient: core.savedObjects.getScopedClient(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes,
|
||||
}),
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
defaultKibanaIndex: await kibanaIndex,
|
||||
scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request),
|
||||
preconfiguredActions,
|
||||
request,
|
||||
authorization: instantiateAuthorization(request),
|
||||
actionExecutor: actionExecutor!,
|
||||
executionEnqueuer: createExecutionEnqueuerFunction({
|
||||
taskManager: plugins.taskManager,
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
|
||||
preconfiguredActions,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>
|
||||
core.savedObjects.getScopedClient(request);
|
||||
|
@ -245,7 +283,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
logger,
|
||||
eventLogger: this.eventLogger!,
|
||||
spaces: this.spaces,
|
||||
getScopedSavedObjectsClient,
|
||||
getActionsClientWithRequest,
|
||||
getServices: this.getServicesFactory(
|
||||
getScopedSavedObjectsClientWithoutAccessToActions,
|
||||
core.elasticsearch
|
||||
|
@ -261,7 +299,10 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
encryptedSavedObjectsClient,
|
||||
getBasePath: this.getBasePath,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
getScopedSavedObjectsClient,
|
||||
getScopedSavedObjectsClient: (request: KibanaRequest) =>
|
||||
core.savedObjects.getScopedClient(request, {
|
||||
includedHiddenTypes,
|
||||
}),
|
||||
});
|
||||
|
||||
scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager);
|
||||
|
@ -273,33 +314,24 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
isActionExecutable: (actionId: string, actionTypeId: string) => {
|
||||
return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId);
|
||||
},
|
||||
// Ability to get an actions client from legacy code
|
||||
async getActionsClientWithRequest(request: KibanaRequest) {
|
||||
if (isESOUsingEphemeralEncryptionKey === true) {
|
||||
throw new Error(
|
||||
`Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
|
||||
);
|
||||
}
|
||||
return new ActionsClient({
|
||||
savedObjectsClient: getScopedSavedObjectsClient(request),
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
defaultKibanaIndex: await kibanaIndex,
|
||||
scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request),
|
||||
preconfiguredActions,
|
||||
request,
|
||||
actionExecutor: actionExecutor!,
|
||||
executionEnqueuer: createExecutionEnqueuerFunction({
|
||||
taskManager: plugins.taskManager,
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
|
||||
preconfiguredActions,
|
||||
}),
|
||||
});
|
||||
getActionsAuthorizationWithRequest(request: KibanaRequest) {
|
||||
return instantiateAuthorization(request);
|
||||
},
|
||||
getActionsClientWithRequest,
|
||||
preconfiguredActions,
|
||||
};
|
||||
}
|
||||
|
||||
private instantiateAuthorization = (request: KibanaRequest) => {
|
||||
return new ActionsAuthorization({
|
||||
request,
|
||||
authorization: this.security?.authz,
|
||||
auditLogger: new ActionsAuthorizationAuditLogger(
|
||||
this.security?.audit.getLogger(ACTIONS_FEATURE.id)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
private getServicesFactory(
|
||||
getScopedClient: (request: KibanaRequest) => SavedObjectsClientContract,
|
||||
elasticsearch: ElasticsearchServiceStart
|
||||
|
@ -322,6 +354,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
isESOUsingEphemeralEncryptionKey,
|
||||
preconfiguredActions,
|
||||
actionExecutor,
|
||||
instantiateAuthorization,
|
||||
} = this;
|
||||
|
||||
return async function actionsRouteHandlerContext(context, request) {
|
||||
|
@ -334,12 +367,16 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
);
|
||||
}
|
||||
return new ActionsClient({
|
||||
savedObjectsClient: savedObjects.getScopedClient(request, { includedHiddenTypes }),
|
||||
unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes,
|
||||
}),
|
||||
actionTypeRegistry: actionTypeRegistry!,
|
||||
defaultKibanaIndex,
|
||||
scopedClusterClient: context.core.elasticsearch.legacy.client,
|
||||
preconfiguredActions,
|
||||
request,
|
||||
authorization: instantiateAuthorization(request),
|
||||
actionExecutor: actionExecutor!,
|
||||
executionEnqueuer: createExecutionEnqueuerFunction({
|
||||
taskManager,
|
||||
|
|
|
@ -28,13 +28,6 @@ describe('createActionRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const createResult = {
|
||||
id: '1',
|
||||
|
|
|
@ -30,9 +30,6 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState)
|
|||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -28,13 +28,6 @@ describe('deleteActionRoute', () => {
|
|||
const [config, handler] = router.delete.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.delete.mockResolvedValueOnce({});
|
||||
|
|
|
@ -31,9 +31,6 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState)
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -53,13 +53,6 @@ describe('executeActionRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(await handler(context, req, res)).toEqual({ body: executeResult });
|
||||
|
||||
|
|
|
@ -32,9 +32,6 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState)
|
|||
body: bodySchema,
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -29,13 +29,6 @@ describe('getActionRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const getResult = {
|
||||
id: '1',
|
||||
|
|
|
@ -26,9 +26,6 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) =>
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -29,13 +29,6 @@ describe('getAllActionRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([]);
|
||||
|
@ -64,13 +57,6 @@ describe('getAllActionRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([]);
|
||||
|
@ -95,13 +81,6 @@ describe('getAllActionRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([]);
|
||||
|
|
|
@ -19,9 +19,6 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState)
|
|||
{
|
||||
path: `${BASE_ACTION_API_PATH}`,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { licenseStateMock } from '../lib/license_state.mock';
|
|||
import { verifyApiAccess } from '../lib';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { LicenseType } from '../../../../plugins/licensing/server';
|
||||
import { actionsClientMock } from '../mocks';
|
||||
|
||||
jest.mock('../lib/verify_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
|
@ -29,13 +30,6 @@ describe('listActionTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
|
@ -48,7 +42,9 @@ describe('listActionTypesRoute', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']);
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -65,8 +61,6 @@ describe('listActionTypesRoute', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(context.actions!.listTypes).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: listTypes,
|
||||
});
|
||||
|
@ -81,13 +75,6 @@ describe('listActionTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
|
@ -100,8 +87,11 @@ describe('listActionTypesRoute', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ listTypes },
|
||||
{ actionsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
|
@ -126,13 +116,6 @@ describe('listActionTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
|
@ -145,8 +128,11 @@ describe('listActionTypesRoute', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ listTypes },
|
||||
{ actionsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
|
|
|
@ -19,9 +19,6 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat
|
|||
{
|
||||
path: `${BASE_ACTION_API_PATH}/list_action_types`,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
@ -32,8 +29,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat
|
|||
if (!context.actions) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
|
||||
}
|
||||
const actionsClient = context.actions.getActionsClient();
|
||||
return res.ok({
|
||||
body: context.actions.listTypes(),
|
||||
body: await actionsClient.listTypes(),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -28,13 +28,6 @@ describe('updateActionRoute', () => {
|
|||
const [config, handler] = router.put.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:actions-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const updateResult = {
|
||||
id: '1',
|
||||
|
|
|
@ -33,9 +33,6 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState)
|
|||
body: bodySchema,
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -8,12 +8,15 @@ import { SavedObjectsServiceSetup } from 'kibana/server';
|
|||
import mappings from './mappings.json';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
|
||||
|
||||
export const ACTION_SAVED_OBJECT_TYPE = 'action';
|
||||
export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params';
|
||||
|
||||
export function setupSavedObjects(
|
||||
savedObjects: SavedObjectsServiceSetup,
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
) {
|
||||
savedObjects.registerType({
|
||||
name: 'action',
|
||||
name: ACTION_SAVED_OBJECT_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'single',
|
||||
mappings: mappings.action,
|
||||
|
@ -24,19 +27,19 @@ export function setupSavedObjects(
|
|||
// - `config` will be included in AAD
|
||||
// - everything else excluded from AAD
|
||||
encryptedSavedObjects.registerType({
|
||||
type: 'action',
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
attributesToEncrypt: new Set(['secrets']),
|
||||
attributesToExcludeFromAAD: new Set(['name']),
|
||||
});
|
||||
|
||||
savedObjects.registerType({
|
||||
name: 'action_task_params',
|
||||
name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'single',
|
||||
mappings: mappings.action_task_params,
|
||||
});
|
||||
encryptedSavedObjects.registerType({
|
||||
type: 'action_task_params',
|
||||
type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
});
|
||||
}
|
||||
|
|
7
x-pack/plugins/alerting_builtins/common/index.ts
Normal file
7
x-pack/plugins/alerting_builtins/common/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const BUILT_IN_ALERTS_FEATURE_ID = 'builtInAlerts';
|
|
@ -3,7 +3,7 @@
|
|||
"server": true,
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"requiredPlugins": ["alerts"],
|
||||
"requiredPlugins": ["alerts", "features"],
|
||||
"configPath": ["xpack", "alerting_builtins"],
|
||||
"ui": false
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ import { AlertType, AlertExecutorOptions } from '../../types';
|
|||
import { Params, ParamsSchema } from './alert_type_params';
|
||||
import { BaseActionContext, addMessages } from './action_context';
|
||||
import { TimeSeriesQuery } from './lib/time_series_query';
|
||||
import { Service } from '../../types';
|
||||
import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../common';
|
||||
|
||||
export const ID = '.index-threshold';
|
||||
|
||||
import { Service } from '../../types';
|
||||
|
||||
const ActionGroupId = 'threshold met';
|
||||
const ComparatorFns = getComparatorFns();
|
||||
export const ComparatorFnNames = new Set(ComparatorFns.keys());
|
||||
|
@ -85,7 +85,7 @@ export function getAlertType(service: Service): AlertType {
|
|||
],
|
||||
},
|
||||
executor,
|
||||
producer: 'alerting',
|
||||
producer: BUILT_IN_ALERTS_FEATURE_ID,
|
||||
};
|
||||
|
||||
async function executor(options: AlertExecutorOptions) {
|
||||
|
|
49
x-pack/plugins/alerting_builtins/server/feature.ts
Normal file
49
x-pack/plugins/alerting_builtins/server/feature.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type';
|
||||
import { BUILT_IN_ALERTS_FEATURE_ID } from '../common';
|
||||
|
||||
export const BUILT_IN_ALERTS_FEATURE = {
|
||||
id: BUILT_IN_ALERTS_FEATURE_ID,
|
||||
name: i18n.translate('xpack.alertingBuiltins.featureRegistry.actionsFeatureName', {
|
||||
defaultMessage: 'Built-In Alerts',
|
||||
}),
|
||||
icon: 'bell',
|
||||
app: [],
|
||||
alerting: [IndexThreshold],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [],
|
||||
catalogue: [],
|
||||
alerting: {
|
||||
all: [IndexThreshold],
|
||||
read: [],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
api: [],
|
||||
ui: ['alerting:show'],
|
||||
},
|
||||
read: {
|
||||
app: [],
|
||||
catalogue: [],
|
||||
alerting: {
|
||||
all: [],
|
||||
read: [IndexThreshold],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
api: [],
|
||||
ui: ['alerting:show'],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
import { PluginInitializerContext } from 'src/core/server';
|
||||
import { AlertingBuiltinsPlugin } from './plugin';
|
||||
import { configSchema } from './config';
|
||||
export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type';
|
||||
|
||||
export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx);
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import { AlertingBuiltinsPlugin } from './plugin';
|
||||
import { coreMock } from '../../../../src/core/server/mocks';
|
||||
import { alertsMock } from '../../alerts/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { BUILT_IN_ALERTS_FEATURE } from './feature';
|
||||
|
||||
describe('AlertingBuiltins Plugin', () => {
|
||||
describe('setup()', () => {
|
||||
|
@ -22,7 +24,8 @@ describe('AlertingBuiltins Plugin', () => {
|
|||
|
||||
it('should register built-in alert types', async () => {
|
||||
const alertingSetup = alertsMock.createSetup();
|
||||
await plugin.setup(coreSetup, { alerts: alertingSetup });
|
||||
const featuresSetup = featuresPluginMock.createSetup();
|
||||
await plugin.setup(coreSetup, { alerts: alertingSetup, features: featuresSetup });
|
||||
|
||||
expect(alertingSetup.registerType).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
@ -40,11 +43,16 @@ describe('AlertingBuiltins Plugin', () => {
|
|||
"name": "Index threshold",
|
||||
}
|
||||
`);
|
||||
expect(featuresSetup.registerFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE);
|
||||
});
|
||||
|
||||
it('should return a service in the expected shape', async () => {
|
||||
const alertingSetup = alertsMock.createSetup();
|
||||
const service = await plugin.setup(coreSetup, { alerts: alertingSetup });
|
||||
const featuresSetup = featuresPluginMock.createSetup();
|
||||
const service = await plugin.setup(coreSetup, {
|
||||
alerts: alertingSetup,
|
||||
features: featuresSetup,
|
||||
});
|
||||
|
||||
expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function');
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from '
|
|||
import { Service, IService, AlertingBuiltinsDeps } from './types';
|
||||
import { getService as getServiceIndexThreshold } from './alert_types/index_threshold';
|
||||
import { registerBuiltInAlertTypes } from './alert_types';
|
||||
import { BUILT_IN_ALERTS_FEATURE } from './feature';
|
||||
|
||||
export class AlertingBuiltinsPlugin implements Plugin<IService, IService> {
|
||||
private readonly logger: Logger;
|
||||
|
@ -22,7 +23,12 @@ export class AlertingBuiltinsPlugin implements Plugin<IService, IService> {
|
|||
};
|
||||
}
|
||||
|
||||
public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise<IService> {
|
||||
public async setup(
|
||||
core: CoreSetup,
|
||||
{ alerts, features }: AlertingBuiltinsDeps
|
||||
): Promise<IService> {
|
||||
features.registerFeature(BUILT_IN_ALERTS_FEATURE);
|
||||
|
||||
registerBuiltInAlertTypes({
|
||||
service: this.service,
|
||||
router: core.http.createRouter(),
|
||||
|
|
|
@ -15,10 +15,12 @@ export {
|
|||
AlertType,
|
||||
AlertExecutorOptions,
|
||||
} from '../../alerts/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
|
||||
// this plugin's dependendencies
|
||||
export interface AlertingBuiltinsDeps {
|
||||
alerts: AlertingSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
}
|
||||
|
||||
// external service exposed through plugin setup/start
|
||||
|
|
|
@ -18,6 +18,7 @@ Table of Contents
|
|||
- [Methods](#methods)
|
||||
- [Executor](#executor)
|
||||
- [Example](#example)
|
||||
- [Role Based Access-Control](#role-based-access-control)
|
||||
- [Alert Navigation](#alert-navigation)
|
||||
- [RESTful API](#restful-api)
|
||||
- [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert)
|
||||
|
@ -58,7 +59,8 @@ A Kibana alert detects a condition and executes one or more actions when that co
|
|||
## Usage
|
||||
|
||||
1. Develop and register an alert type (see alert types -> example).
|
||||
2. Create an alert using the RESTful API (see alerts -> create).
|
||||
2. Configure feature level privileges using RBAC
|
||||
3. Create an alert using the RESTful API (see alerts -> create).
|
||||
|
||||
## Limitations
|
||||
|
||||
|
@ -293,6 +295,111 @@ server.newPlatform.setup.plugins.alerts.registerType({
|
|||
});
|
||||
```
|
||||
|
||||
## Role Based Access-Control
|
||||
Once you have registered your AlertType, you need to grant your users privileges to use it.
|
||||
When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles.
|
||||
|
||||
Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for these AlertTypes when they're inside the feature.
|
||||
In addition, when users are inside your feature you might want to grant them access to AlertTypes from other features, such as built-in AlertTypes or AlertTypes provided by other features.
|
||||
|
||||
You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example:
|
||||
|
||||
```typescript
|
||||
features.registerFeature({
|
||||
id: 'my-application-id',
|
||||
name: 'My Application',
|
||||
app: [],
|
||||
privileges: {
|
||||
all: {
|
||||
alerting: {
|
||||
all: [
|
||||
// grant `all` over our own types
|
||||
'my-application-id.my-alert-type',
|
||||
'my-application-id.my-restricted-alert-type',
|
||||
// grant `all` over the built-in IndexThreshold
|
||||
'.index-threshold',
|
||||
// grant `all` over Uptime's TLS AlertType
|
||||
'xpack.uptime.alerts.actionGroups.tls'
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
alerting: {
|
||||
read: [
|
||||
// grant `read` over our own type
|
||||
'my-application-id.my-alert-type',
|
||||
// grant `read` over the built-in IndexThreshold
|
||||
'.index-threshold',
|
||||
// grant `read` over Uptime's TLS AlertType
|
||||
'xpack.uptime.alerts.actionGroups.tls'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In this example we can see the following:
|
||||
- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts.
|
||||
- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type.
|
||||
- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user.
|
||||
|
||||
It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example:
|
||||
|
||||
```typescript
|
||||
features.registerFeature({
|
||||
id: 'my-application-id',
|
||||
name: 'My Application',
|
||||
app: [],
|
||||
privileges: {
|
||||
all: {
|
||||
alerting: {
|
||||
all: [
|
||||
'my-application-id.my-alert-type',
|
||||
'my-application-id.my-restricted-alert-type'
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
alerting: {
|
||||
all: [
|
||||
'my-application-id.my-alert-type'
|
||||
]
|
||||
read: [
|
||||
'my-application-id.my-restricted-alert-type'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType.
|
||||
As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it.
|
||||
|
||||
### `read` privileges vs. `all` privileges
|
||||
When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls:
|
||||
- `get`
|
||||
- `getAlertState`
|
||||
- `find`
|
||||
|
||||
When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls:
|
||||
- `create`
|
||||
- `delete`
|
||||
- `update`
|
||||
- `enable`
|
||||
- `disable`
|
||||
- `updateApiKey`
|
||||
- `muteAll`
|
||||
- `unmuteAll`
|
||||
- `muteInstance`
|
||||
- `unmuteInstance`
|
||||
|
||||
Finally, all users, whether they're granted any role or not, are privileged to call the following:
|
||||
- `listAlertTypes`, but the output is limited to displaying the AlertTypes the user is perivileged to `get`
|
||||
|
||||
Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error thrown by the AlertsClient.
|
||||
|
||||
## Alert Navigation
|
||||
When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI.
|
||||
|
||||
|
|
|
@ -21,3 +21,4 @@ export interface AlertingFrameworkHealth {
|
|||
}
|
||||
|
||||
export const BASE_ALERT_API_PATH = '/api/alerts';
|
||||
export const ALERTS_FEATURE_ID = 'alerts';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "alerts"],
|
||||
"requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"],
|
||||
"requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog", "features"],
|
||||
"optionalPlugins": ["usageCollection", "spaces", "security"],
|
||||
"extraPublicDirs": ["common", "common/parse_duration"]
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('loadAlertTypes', () => {
|
|||
actionVariables: ['var1'],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
},
|
||||
];
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
@ -45,7 +45,7 @@ describe('loadAlertType', () => {
|
|||
actionVariables: ['var1'],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
http.get.mockResolvedValueOnce([alertType]);
|
||||
|
||||
|
@ -65,7 +65,7 @@ describe('loadAlertType', () => {
|
|||
actionVariables: [],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
http.get.mockResolvedValueOnce([alertType]);
|
||||
|
||||
|
@ -80,7 +80,7 @@ describe('loadAlertType', () => {
|
|||
actionVariables: [],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const mockAlertType = (id: string): AlertType => ({
|
|||
actionGroups: [],
|
||||
actionVariables: [],
|
||||
defaultActionGroupId: 'default',
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
});
|
||||
|
||||
describe('AlertNavigationRegistry', () => {
|
||||
|
|
|
@ -36,13 +36,67 @@ describe('has()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
});
|
||||
expect(registry.has('foo')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register()', () => {
|
||||
test('throws if AlertType Id contains invalid characters', () => {
|
||||
const alertType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
|
||||
const invalidCharacters = [' ', ':', '*', '*', '/'];
|
||||
for (const char of invalidCharacters) {
|
||||
expect(() => registry.register({ ...alertType, id: `${alertType.id}${char}` })).toThrowError(
|
||||
new Error(`expected AlertType Id not to include invalid character: ${char}`)
|
||||
);
|
||||
}
|
||||
|
||||
const [first, second] = invalidCharacters;
|
||||
expect(() =>
|
||||
registry.register({ ...alertType, id: `${first}${alertType.id}${second}` })
|
||||
).toThrowError(
|
||||
new Error(`expected AlertType Id not to include invalid characters: ${first}, ${second}`)
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if AlertType Id isnt a string', () => {
|
||||
const alertType = {
|
||||
id: (123 as unknown) as string,
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
|
||||
expect(() => registry.register(alertType)).toThrowError(
|
||||
new Error(`expected value of type [string] but got [number]`)
|
||||
);
|
||||
});
|
||||
|
||||
test('registers the executor with the task manager', () => {
|
||||
const alertType = {
|
||||
id: 'test',
|
||||
|
@ -55,7 +109,7 @@ describe('register()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
|
@ -86,7 +140,7 @@ describe('register()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
registry.register(alertType);
|
||||
|
@ -107,7 +161,7 @@ describe('register()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
});
|
||||
expect(() =>
|
||||
registry.register({
|
||||
|
@ -121,7 +175,7 @@ describe('register()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Alert type \\"test\\" is already registered."`);
|
||||
});
|
||||
|
@ -141,7 +195,7 @@ describe('get()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
});
|
||||
const alertType = registry.get('test');
|
||||
expect(alertType).toMatchInlineSnapshot(`
|
||||
|
@ -160,7 +214,7 @@ describe('get()', () => {
|
|||
"executor": [MockFunction],
|
||||
"id": "test",
|
||||
"name": "Test",
|
||||
"producer": "alerting",
|
||||
"producer": "alerts",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -177,7 +231,7 @@ describe('list()', () => {
|
|||
test('should return empty when nothing is registered', () => {
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
const result = registry.list();
|
||||
expect(result).toMatchInlineSnapshot(`Array []`);
|
||||
expect(result).toMatchInlineSnapshot(`Set {}`);
|
||||
});
|
||||
|
||||
test('should return registered types', () => {
|
||||
|
@ -193,11 +247,11 @@ describe('list()', () => {
|
|||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
});
|
||||
const result = registry.list();
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Set {
|
||||
Object {
|
||||
"actionGroups": Array [
|
||||
Object {
|
||||
|
@ -212,9 +266,9 @@ describe('list()', () => {
|
|||
"defaultActionGroupId": "testActionGroup",
|
||||
"id": "test",
|
||||
"name": "Test",
|
||||
"producer": "alerting",
|
||||
"producer": "alerts",
|
||||
},
|
||||
]
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -260,7 +314,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale
|
|||
actionGroups: [],
|
||||
defaultActionGroupId: id,
|
||||
async executor() {},
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
|
||||
if (!context && !state) {
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import typeDetect from 'type-detect';
|
||||
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
|
||||
import { TaskRunnerFactory } from './task_runner';
|
||||
import { AlertType } from './types';
|
||||
|
@ -15,6 +17,34 @@ interface ConstructorOptions {
|
|||
taskRunnerFactory: TaskRunnerFactory;
|
||||
}
|
||||
|
||||
export interface RegistryAlertType
|
||||
extends Pick<
|
||||
AlertType,
|
||||
'name' | 'actionGroups' | 'defaultActionGroupId' | 'actionVariables' | 'producer'
|
||||
> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertType IDs are used as part of the authorization strings used to
|
||||
* grant users privileged operations. There is a limited range of characters
|
||||
* we can use in these auth strings, so we apply these same limitations to
|
||||
* the AlertType Ids.
|
||||
* If you wish to change this, please confer with the Kibana security team.
|
||||
*/
|
||||
const alertIdSchema = schema.string({
|
||||
validate(value: string): string | void {
|
||||
if (typeof value !== 'string') {
|
||||
return `expected AlertType Id of type [string] but got [${typeDetect(value)}]`;
|
||||
} else if (!value.match(/^[a-zA-Z0-9_\-\.]*$/)) {
|
||||
const invalid = value.match(/[^a-zA-Z0-9_\-\.]+/g)!;
|
||||
return `expected AlertType Id not to include invalid character${
|
||||
invalid.length > 1 ? `s` : ``
|
||||
}: ${invalid?.join(`, `)}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export class AlertTypeRegistry {
|
||||
private readonly taskManager: TaskManagerSetupContract;
|
||||
private readonly alertTypes: Map<string, AlertType> = new Map();
|
||||
|
@ -41,7 +71,7 @@ export class AlertTypeRegistry {
|
|||
);
|
||||
}
|
||||
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
|
||||
this.alertTypes.set(alertType.id, { ...alertType });
|
||||
this.alertTypes.set(alertIdSchema.validate(alertType.id), { ...alertType });
|
||||
this.taskManager.registerTaskDefinitions({
|
||||
[`alerting:${alertType.id}`]: {
|
||||
title: alertType.name,
|
||||
|
@ -66,15 +96,22 @@ export class AlertTypeRegistry {
|
|||
return this.alertTypes.get(id)!;
|
||||
}
|
||||
|
||||
public list() {
|
||||
return Array.from(this.alertTypes).map(([alertTypeId, alertType]) => ({
|
||||
id: alertTypeId,
|
||||
name: alertType.name,
|
||||
actionGroups: alertType.actionGroups,
|
||||
defaultActionGroupId: alertType.defaultActionGroupId,
|
||||
actionVariables: alertType.actionVariables,
|
||||
producer: alertType.producer,
|
||||
}));
|
||||
public list(): Set<RegistryAlertType> {
|
||||
return new Set(
|
||||
Array.from(this.alertTypes).map(
|
||||
([id, { name, actionGroups, defaultActionGroupId, actionVariables, producer }]: [
|
||||
string,
|
||||
AlertType
|
||||
]) => ({
|
||||
id,
|
||||
name,
|
||||
actionGroups,
|
||||
defaultActionGroupId,
|
||||
actionVariables,
|
||||
producer,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ const createAlertsClientMock = () => {
|
|||
unmuteAll: jest.fn(),
|
||||
muteInstance: jest.fn(),
|
||||
unmuteInstance: jest.fn(),
|
||||
listAlertTypes: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { omit, isEqual, map, truncate } from 'lodash';
|
||||
import { omit, isEqual, map, uniq, pick, truncate } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Logger,
|
||||
|
@ -13,7 +13,7 @@ import {
|
|||
SavedObjectReference,
|
||||
SavedObject,
|
||||
} from 'src/core/server';
|
||||
import { ActionsClient } from '../../actions/server';
|
||||
import { ActionsClient, ActionsAuthorization } from '../../actions/server';
|
||||
import {
|
||||
Alert,
|
||||
PartialAlert,
|
||||
|
@ -35,7 +35,16 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve
|
|||
import { TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
|
||||
import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists';
|
||||
import { RegistryAlertType } from './alert_type_registry';
|
||||
import {
|
||||
AlertsAuthorization,
|
||||
WriteOperations,
|
||||
ReadOperations,
|
||||
} from './authorization/alerts_authorization';
|
||||
|
||||
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
|
||||
authorizedConsumers: string[];
|
||||
}
|
||||
type NormalizedAlertAction = Omit<AlertAction, 'actionTypeId'>;
|
||||
export type CreateAPIKeyResult =
|
||||
| { apiKeysEnabled: false }
|
||||
|
@ -44,10 +53,12 @@ export type InvalidateAPIKeyResult =
|
|||
| { apiKeysEnabled: false }
|
||||
| { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };
|
||||
|
||||
interface ConstructorOptions {
|
||||
export interface ConstructorOptions {
|
||||
logger: Logger;
|
||||
taskManager: TaskManagerStartContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
authorization: AlertsAuthorization;
|
||||
actionsAuthorization: ActionsAuthorization;
|
||||
alertTypeRegistry: AlertTypeRegistry;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
spaceId?: string;
|
||||
|
@ -127,18 +138,21 @@ export class AlertsClient {
|
|||
private readonly spaceId?: string;
|
||||
private readonly namespace?: string;
|
||||
private readonly taskManager: TaskManagerStartContract;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly authorization: AlertsAuthorization;
|
||||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly createAPIKey: (name: string) => Promise<CreateAPIKeyResult>;
|
||||
private readonly invalidateAPIKey: (
|
||||
params: InvalidateAPIKeyParams
|
||||
) => Promise<InvalidateAPIKeyResult>;
|
||||
private readonly getActionsClient: () => Promise<ActionsClient>;
|
||||
private readonly actionsAuthorization: ActionsAuthorization;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
|
||||
constructor({
|
||||
alertTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
taskManager,
|
||||
logger,
|
||||
spaceId,
|
||||
|
@ -148,6 +162,7 @@ export class AlertsClient {
|
|||
invalidateAPIKey,
|
||||
encryptedSavedObjectsClient,
|
||||
getActionsClient,
|
||||
actionsAuthorization,
|
||||
}: ConstructorOptions) {
|
||||
this.logger = logger;
|
||||
this.getUserName = getUserName;
|
||||
|
@ -155,16 +170,25 @@ export class AlertsClient {
|
|||
this.namespace = namespace;
|
||||
this.taskManager = taskManager;
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
|
||||
this.authorization = authorization;
|
||||
this.createAPIKey = createAPIKey;
|
||||
this.invalidateAPIKey = invalidateAPIKey;
|
||||
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
|
||||
this.getActionsClient = getActionsClient;
|
||||
this.actionsAuthorization = actionsAuthorization;
|
||||
}
|
||||
|
||||
public async create({ data, options }: CreateOptions): Promise<Alert> {
|
||||
await this.authorization.ensureAuthorized(
|
||||
data.alertTypeId,
|
||||
data.consumer,
|
||||
WriteOperations.Create
|
||||
);
|
||||
|
||||
// Throws an error if alert type isn't registered
|
||||
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
|
||||
|
||||
const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params);
|
||||
const username = await this.getUserName();
|
||||
|
||||
|
@ -186,7 +210,7 @@ export class AlertsClient {
|
|||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
};
|
||||
const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, {
|
||||
const createdAlert = await this.unsecuredSavedObjectsClient.create('alert', rawAlert, {
|
||||
...options,
|
||||
references,
|
||||
});
|
||||
|
@ -197,7 +221,7 @@ export class AlertsClient {
|
|||
} catch (e) {
|
||||
// Cleanup data, something went wrong scheduling the task
|
||||
try {
|
||||
await this.savedObjectsClient.delete('alert', createdAlert.id);
|
||||
await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id);
|
||||
} catch (err) {
|
||||
// Skip the cleanup error and throw the task manager error to avoid confusion
|
||||
this.logger.error(
|
||||
|
@ -206,7 +230,7 @@ export class AlertsClient {
|
|||
}
|
||||
throw e;
|
||||
}
|
||||
await this.savedObjectsClient.update('alert', createdAlert.id, {
|
||||
await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, {
|
||||
scheduledTaskId: scheduledTask.id,
|
||||
});
|
||||
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
|
||||
|
@ -220,12 +244,22 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async get({ id }: { id: string }): Promise<SanitizedAlert> {
|
||||
const result = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const result = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
await this.authorization.ensureAuthorized(
|
||||
result.attributes.alertTypeId,
|
||||
result.attributes.consumer,
|
||||
ReadOperations.Get
|
||||
);
|
||||
return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references);
|
||||
}
|
||||
|
||||
public async getAlertState({ id }: { id: string }): Promise<AlertTaskState | void> {
|
||||
const alert = await this.get({ id });
|
||||
await this.authorization.ensureAuthorized(
|
||||
alert.alertTypeId,
|
||||
alert.consumer,
|
||||
ReadOperations.GetAlertState
|
||||
);
|
||||
if (alert.scheduledTaskId) {
|
||||
const { state } = taskInstanceToAlertTaskInstance(
|
||||
await this.taskManager.get(alert.scheduledTaskId),
|
||||
|
@ -235,30 +269,56 @@ export class AlertsClient {
|
|||
}
|
||||
}
|
||||
|
||||
public async find({ options = {} }: { options: FindOptions }): Promise<FindResult> {
|
||||
public async find({
|
||||
options: { fields, ...options } = {},
|
||||
}: { options?: FindOptions } = {}): Promise<FindResult> {
|
||||
const {
|
||||
filter: authorizationFilter,
|
||||
ensureAlertTypeIsAuthorized,
|
||||
logSuccessfulAuthorization,
|
||||
} = await this.authorization.getFindAuthorizationFilter();
|
||||
|
||||
if (authorizationFilter) {
|
||||
options.filter = options.filter
|
||||
? `${options.filter} and ${authorizationFilter}`
|
||||
: authorizationFilter;
|
||||
}
|
||||
|
||||
const {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total,
|
||||
saved_objects: data,
|
||||
} = await this.savedObjectsClient.find<RawAlert>({
|
||||
} = await this.unsecuredSavedObjectsClient.find<RawAlert>({
|
||||
...options,
|
||||
fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields,
|
||||
type: 'alert',
|
||||
});
|
||||
|
||||
const authorizedData = data.map(({ id, attributes, updated_at, references }) => {
|
||||
ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
|
||||
return this.getAlertFromRaw(
|
||||
id,
|
||||
fields ? (pick(attributes, fields) as RawAlert) : attributes,
|
||||
updated_at,
|
||||
references
|
||||
);
|
||||
});
|
||||
|
||||
logSuccessfulAuthorization();
|
||||
|
||||
return {
|
||||
page,
|
||||
perPage,
|
||||
total,
|
||||
data: data.map(({ id, attributes, updated_at, references }) =>
|
||||
this.getAlertFromRaw(id, attributes, updated_at, references)
|
||||
),
|
||||
data: authorizedData,
|
||||
};
|
||||
}
|
||||
|
||||
public async delete({ id }: { id: string }) {
|
||||
let taskIdToRemove: string | undefined;
|
||||
let apiKeyToInvalidate: string | null = null;
|
||||
let attributes: RawAlert;
|
||||
|
||||
try {
|
||||
const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<
|
||||
|
@ -266,17 +326,25 @@ export class AlertsClient {
|
|||
>('alert', id, { namespace: this.namespace });
|
||||
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
|
||||
taskIdToRemove = decryptedAlert.attributes.scheduledTaskId;
|
||||
attributes = decryptedAlert.attributes;
|
||||
} catch (e) {
|
||||
// We'll skip invalidating the API key since we failed to load the decrypted saved object
|
||||
this.logger.error(
|
||||
`delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
// Still attempt to load the scheduledTaskId using SOC
|
||||
const alert = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
taskIdToRemove = alert.attributes.scheduledTaskId;
|
||||
attributes = alert.attributes;
|
||||
}
|
||||
|
||||
const removeResult = await this.savedObjectsClient.delete('alert', id);
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.Delete
|
||||
);
|
||||
|
||||
const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id);
|
||||
|
||||
await Promise.all([
|
||||
taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null,
|
||||
|
@ -299,8 +367,13 @@ export class AlertsClient {
|
|||
`update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
// Still attempt to load the object using SOC
|
||||
alertSavedObject = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
alertSavedObject = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
}
|
||||
await this.authorization.ensureAuthorized(
|
||||
alertSavedObject.attributes.alertTypeId,
|
||||
alertSavedObject.attributes.consumer,
|
||||
WriteOperations.Update
|
||||
);
|
||||
|
||||
const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
|
||||
|
||||
|
@ -342,7 +415,7 @@ export class AlertsClient {
|
|||
: null;
|
||||
const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username);
|
||||
|
||||
const updatedObject = await this.savedObjectsClient.update<RawAlert>(
|
||||
const updatedObject = await this.unsecuredSavedObjectsClient.update<RawAlert>(
|
||||
'alert',
|
||||
id,
|
||||
{
|
||||
|
@ -400,13 +473,22 @@ export class AlertsClient {
|
|||
`updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
}
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.UpdateApiKey
|
||||
);
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
const username = await this.getUserName();
|
||||
await this.savedObjectsClient.update(
|
||||
await this.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
id,
|
||||
{
|
||||
|
@ -459,14 +541,24 @@ export class AlertsClient {
|
|||
`enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
}
|
||||
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.Enable
|
||||
);
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
if (attributes.enabled === false) {
|
||||
const username = await this.getUserName();
|
||||
await this.savedObjectsClient.update(
|
||||
await this.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
id,
|
||||
{
|
||||
|
@ -483,7 +575,9 @@ export class AlertsClient {
|
|||
{ version }
|
||||
);
|
||||
const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId);
|
||||
await this.savedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id });
|
||||
await this.unsecuredSavedObjectsClient.update('alert', id, {
|
||||
scheduledTaskId: scheduledTask.id,
|
||||
});
|
||||
if (apiKeyToInvalidate) {
|
||||
await this.invalidateApiKey({ apiKey: apiKeyToInvalidate });
|
||||
}
|
||||
|
@ -508,13 +602,19 @@ export class AlertsClient {
|
|||
`disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
}
|
||||
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.Disable
|
||||
);
|
||||
|
||||
if (attributes.enabled === true) {
|
||||
await this.savedObjectsClient.update(
|
||||
await this.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
id,
|
||||
{
|
||||
|
@ -538,7 +638,18 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async muteAll({ id }: { id: string }) {
|
||||
await this.savedObjectsClient.update('alert', id, {
|
||||
const { attributes } = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.MuteAll
|
||||
);
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
await this.unsecuredSavedObjectsClient.update('alert', id, {
|
||||
muteAll: true,
|
||||
mutedInstanceIds: [],
|
||||
updatedBy: await this.getUserName(),
|
||||
|
@ -546,7 +657,18 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async unmuteAll({ id }: { id: string }) {
|
||||
await this.savedObjectsClient.update('alert', id, {
|
||||
const { attributes } = await this.unsecuredSavedObjectsClient.get<RawAlert>('alert', id);
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.UnmuteAll
|
||||
);
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
await this.unsecuredSavedObjectsClient.update('alert', id, {
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
updatedBy: await this.getUserName(),
|
||||
|
@ -554,11 +676,25 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async muteInstance({ alertId, alertInstanceId }: MuteOptions) {
|
||||
const { attributes, version } = await this.savedObjectsClient.get<Alert>('alert', alertId);
|
||||
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<Alert>(
|
||||
'alert',
|
||||
alertId
|
||||
);
|
||||
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.MuteInstance
|
||||
);
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
const mutedInstanceIds = attributes.mutedInstanceIds || [];
|
||||
if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
|
||||
mutedInstanceIds.push(alertInstanceId);
|
||||
await this.savedObjectsClient.update(
|
||||
await this.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
alertId,
|
||||
{
|
||||
|
@ -577,10 +713,22 @@ export class AlertsClient {
|
|||
alertId: string;
|
||||
alertInstanceId: string;
|
||||
}) {
|
||||
const { attributes, version } = await this.savedObjectsClient.get<Alert>('alert', alertId);
|
||||
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<Alert>(
|
||||
'alert',
|
||||
alertId
|
||||
);
|
||||
await this.authorization.ensureAuthorized(
|
||||
attributes.alertTypeId,
|
||||
attributes.consumer,
|
||||
WriteOperations.UnmuteInstance
|
||||
);
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
|
||||
const mutedInstanceIds = attributes.mutedInstanceIds || [];
|
||||
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
|
||||
await this.savedObjectsClient.update(
|
||||
await this.unsecuredSavedObjectsClient.update(
|
||||
'alert',
|
||||
alertId,
|
||||
{
|
||||
|
@ -593,6 +741,13 @@ export class AlertsClient {
|
|||
}
|
||||
}
|
||||
|
||||
public async listAlertTypes() {
|
||||
return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [
|
||||
ReadOperations.Get,
|
||||
WriteOperations.Create,
|
||||
]);
|
||||
}
|
||||
|
||||
private async scheduleAlert(id: string, alertTypeId: string) {
|
||||
return await this.taskManager.schedule({
|
||||
taskType: `alerting:${alertTypeId}`,
|
||||
|
@ -610,13 +765,14 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
private injectReferencesIntoActions(
|
||||
alertId: string,
|
||||
actions: RawAlert['actions'],
|
||||
references: SavedObjectReference[]
|
||||
) {
|
||||
return actions.map((action) => {
|
||||
const reference = references.find((ref) => ref.name === action.actionRef);
|
||||
if (!reference) {
|
||||
throw new Error(`Reference ${action.actionRef} not found`);
|
||||
throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
|
||||
}
|
||||
return {
|
||||
...omit(action, 'actionRef'),
|
||||
|
@ -639,8 +795,8 @@ export class AlertsClient {
|
|||
|
||||
private getPartialAlertFromRaw(
|
||||
id: string,
|
||||
rawAlert: Partial<RawAlert>,
|
||||
updatedAt: SavedObject['updated_at'],
|
||||
{ createdAt, ...rawAlert }: Partial<RawAlert>,
|
||||
updatedAt: SavedObject['updated_at'] = createdAt,
|
||||
references: SavedObjectReference[] | undefined
|
||||
): PartialAlert {
|
||||
return {
|
||||
|
@ -649,11 +805,11 @@ export class AlertsClient {
|
|||
// we currently only support the Interval Schedule type
|
||||
// Once we support additional types, this type signature will likely change
|
||||
schedule: rawAlert.schedule as IntervalSchedule,
|
||||
updatedAt: updatedAt ? new Date(updatedAt) : new Date(rawAlert.createdAt!),
|
||||
createdAt: new Date(rawAlert.createdAt!),
|
||||
actions: rawAlert.actions
|
||||
? this.injectReferencesIntoActions(rawAlert.actions, references || [])
|
||||
? this.injectReferencesIntoActions(id, rawAlert.actions, references || [])
|
||||
: [],
|
||||
...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
|
||||
...(createdAt ? { createdAt: new Date(createdAt) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -679,38 +835,45 @@ export class AlertsClient {
|
|||
private async denormalizeActions(
|
||||
alertActions: NormalizedAlertAction[]
|
||||
): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> {
|
||||
const actionsClient = await this.getActionsClient();
|
||||
const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
|
||||
const actionResults = await actionsClient.getBulk(actionIds);
|
||||
const references: SavedObjectReference[] = [];
|
||||
const actions = alertActions.map(({ id, ...alertAction }, i) => {
|
||||
const actionResultValue = actionResults.find((action) => action.id === id);
|
||||
if (actionResultValue) {
|
||||
const actionRef = `action_${i}`;
|
||||
references.push({
|
||||
id,
|
||||
name: actionRef,
|
||||
type: 'action',
|
||||
});
|
||||
return {
|
||||
...alertAction,
|
||||
actionRef,
|
||||
actionTypeId: actionResultValue.actionTypeId,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...alertAction,
|
||||
actionRef: '',
|
||||
actionTypeId: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
const actions: RawAlert['actions'] = [];
|
||||
if (alertActions.length) {
|
||||
const actionsClient = await this.getActionsClient();
|
||||
const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
|
||||
const actionResults = await actionsClient.getBulk(actionIds);
|
||||
alertActions.forEach(({ id, ...alertAction }, i) => {
|
||||
const actionResultValue = actionResults.find((action) => action.id === id);
|
||||
if (actionResultValue) {
|
||||
const actionRef = `action_${i}`;
|
||||
references.push({
|
||||
id,
|
||||
name: actionRef,
|
||||
type: 'action',
|
||||
});
|
||||
actions.push({
|
||||
...alertAction,
|
||||
actionRef,
|
||||
actionTypeId: actionResultValue.actionTypeId,
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
...alertAction,
|
||||
actionRef: '',
|
||||
actionTypeId: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
actions,
|
||||
references,
|
||||
};
|
||||
}
|
||||
|
||||
private includeFieldsRequiredForAuthentication(fields: string[]): string[] {
|
||||
return uniq([...fields, 'alertTypeId', 'consumer']);
|
||||
}
|
||||
|
||||
private generateAPIKeyName(alertTypeId: string, alertName: string) {
|
||||
return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 });
|
||||
}
|
||||
|
|
|
@ -9,24 +9,39 @@ import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_fa
|
|||
import { alertTypeRegistryMock } from './alert_type_registry.mock';
|
||||
import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
|
||||
import { KibanaRequest } from '../../../../src/core/server';
|
||||
import { loggingSystemMock, savedObjectsClientMock } from '../../../../src/core/server/mocks';
|
||||
import {
|
||||
savedObjectsClientMock,
|
||||
savedObjectsServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '../../../../src/core/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
|
||||
import { AuthenticatedUser } from '../../../plugins/security/common/model';
|
||||
import { securityMock } from '../../security/server/mocks';
|
||||
import { actionsMock } from '../../actions/server/mocks';
|
||||
import { PluginStartContract as ActionsStartContract } from '../../actions/server';
|
||||
import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { AuditLogger } from '../../security/server';
|
||||
import { ALERTS_FEATURE_ID } from '../common';
|
||||
|
||||
jest.mock('./alerts_client');
|
||||
jest.mock('./authorization/alerts_authorization');
|
||||
jest.mock('./authorization/audit_logger');
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const savedObjectsService = savedObjectsServiceMock.createInternalStartContract();
|
||||
const features = featuresPluginMock.createStart();
|
||||
|
||||
const securityPluginSetup = securityMock.createSetup();
|
||||
const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryOpts> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
taskManager: taskManagerMock.start(),
|
||||
alertTypeRegistry: alertTypeRegistryMock.create(),
|
||||
getSpaceId: jest.fn(),
|
||||
getSpace: jest.fn(),
|
||||
spaceIdToNamespace: jest.fn(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
|
||||
actions: actionsMock.createStart(),
|
||||
features,
|
||||
};
|
||||
const fakeRequest = ({
|
||||
headers: {},
|
||||
|
@ -44,19 +59,101 @@ const fakeRequest = ({
|
|||
getSavedObjectsClient: () => savedObjectsClient,
|
||||
} as unknown) as Request;
|
||||
|
||||
const actionsAuthorization = actionsAuthorizationMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertsClientFactoryParams.actions = actionsMock.createStart();
|
||||
(alertsClientFactoryParams.actions as jest.Mocked<
|
||||
ActionsStartContract
|
||||
>).getActionsAuthorizationWithRequest.mockReturnValue(actionsAuthorization);
|
||||
alertsClientFactoryParams.getSpaceId.mockReturnValue('default');
|
||||
alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default');
|
||||
});
|
||||
|
||||
test('creates an alerts client with proper constructor arguments when security is enabled', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams });
|
||||
const request = KibanaRequest.from(fakeRequest);
|
||||
|
||||
const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger');
|
||||
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
|
||||
|
||||
const logger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
securityPluginSetup.audit.getLogger.mockReturnValue(logger);
|
||||
|
||||
factory.create(request, savedObjectsService);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
});
|
||||
|
||||
const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
|
||||
expect(AlertsAuthorization).toHaveBeenCalledWith({
|
||||
request,
|
||||
authorization: securityPluginSetup.authz,
|
||||
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
|
||||
features: alertsClientFactoryParams.features,
|
||||
auditLogger: expect.any(AlertsAuthorizationAuditLogger),
|
||||
getSpace: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger);
|
||||
expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID);
|
||||
|
||||
expect(alertsClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith(
|
||||
request
|
||||
);
|
||||
|
||||
expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
authorization: expect.any(AlertsAuthorization),
|
||||
actionsAuthorization,
|
||||
logger: alertsClientFactoryParams.logger,
|
||||
taskManager: alertsClientFactoryParams.taskManager,
|
||||
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: expect.any(Function),
|
||||
getActionsClient: expect.any(Function),
|
||||
createAPIKey: expect.any(Function),
|
||||
invalidateAPIKey: expect.any(Function),
|
||||
encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient,
|
||||
});
|
||||
});
|
||||
|
||||
test('creates an alerts client with proper constructor arguments', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
const request = KibanaRequest.from(fakeRequest);
|
||||
|
||||
savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
|
||||
|
||||
factory.create(request, savedObjectsService);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
});
|
||||
|
||||
const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
|
||||
const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger');
|
||||
expect(AlertsAuthorization).toHaveBeenCalledWith({
|
||||
request,
|
||||
authorization: undefined,
|
||||
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
|
||||
features: alertsClientFactoryParams.features,
|
||||
auditLogger: expect.any(AlertsAuthorizationAuditLogger),
|
||||
getSpace: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient: savedObjectsClient,
|
||||
authorization: expect.any(AlertsAuthorization),
|
||||
actionsAuthorization,
|
||||
logger: alertsClientFactoryParams.logger,
|
||||
taskManager: alertsClientFactoryParams.taskManager,
|
||||
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
|
||||
|
@ -73,7 +170,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
|
|||
test('getUserName() returns null when security is disabled', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
const userNameResult = await constructorCall.getUserName();
|
||||
|
@ -86,7 +183,7 @@ test('getUserName() returns a name when security is enabled', async () => {
|
|||
...alertsClientFactoryParams,
|
||||
securityPluginSetup,
|
||||
});
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({
|
||||
|
@ -99,7 +196,7 @@ test('getUserName() returns a name when security is enabled', async () => {
|
|||
test('getActionsClient() returns ActionsClient', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
const actionsClient = await constructorCall.getActionsClient();
|
||||
|
@ -109,7 +206,7 @@ test('getActionsClient() returns ActionsClient', async () => {
|
|||
test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
const createAPIKeyResult = await constructorCall.createAPIKey();
|
||||
|
@ -119,7 +216,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled
|
|||
test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null);
|
||||
|
@ -133,7 +230,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
|
|||
...alertsClientFactoryParams,
|
||||
securityPluginSetup,
|
||||
});
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({
|
||||
|
@ -154,7 +251,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error',
|
|||
...alertsClientFactoryParams,
|
||||
securityPluginSetup,
|
||||
});
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
|
||||
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
|
||||
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce(
|
||||
|
|
|
@ -6,11 +6,16 @@
|
|||
|
||||
import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
import { ALERTS_FEATURE_ID } from '../common';
|
||||
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
|
||||
import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server';
|
||||
import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server';
|
||||
import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server';
|
||||
import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server';
|
||||
import { TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
import { AlertsAuthorization } from './authorization/alerts_authorization';
|
||||
import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger';
|
||||
import { Space } from '../../spaces/server';
|
||||
|
||||
export interface AlertsClientFactoryOpts {
|
||||
logger: Logger;
|
||||
|
@ -18,9 +23,11 @@ export interface AlertsClientFactoryOpts {
|
|||
alertTypeRegistry: AlertTypeRegistry;
|
||||
securityPluginSetup?: SecurityPluginSetup;
|
||||
getSpaceId: (request: KibanaRequest) => string | undefined;
|
||||
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
|
||||
actions: ActionsPluginStartContract;
|
||||
features: FeaturesPluginStart;
|
||||
}
|
||||
|
||||
export class AlertsClientFactory {
|
||||
|
@ -30,9 +37,11 @@ export class AlertsClientFactory {
|
|||
private alertTypeRegistry!: AlertTypeRegistry;
|
||||
private securityPluginSetup?: SecurityPluginSetup;
|
||||
private getSpaceId!: (request: KibanaRequest) => string | undefined;
|
||||
private getSpace!: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
private spaceIdToNamespace!: SpaceIdToNamespaceFunction;
|
||||
private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient;
|
||||
private actions!: ActionsPluginStartContract;
|
||||
private features!: FeaturesPluginStart;
|
||||
|
||||
public initialize(options: AlertsClientFactoryOpts) {
|
||||
if (this.isInitialized) {
|
||||
|
@ -41,26 +50,41 @@ export class AlertsClientFactory {
|
|||
this.isInitialized = true;
|
||||
this.logger = options.logger;
|
||||
this.getSpaceId = options.getSpaceId;
|
||||
this.getSpace = options.getSpace;
|
||||
this.taskManager = options.taskManager;
|
||||
this.alertTypeRegistry = options.alertTypeRegistry;
|
||||
this.securityPluginSetup = options.securityPluginSetup;
|
||||
this.spaceIdToNamespace = options.spaceIdToNamespace;
|
||||
this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient;
|
||||
this.actions = options.actions;
|
||||
this.features = options.features;
|
||||
}
|
||||
|
||||
public create(
|
||||
request: KibanaRequest,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): AlertsClient {
|
||||
const { securityPluginSetup, actions } = this;
|
||||
public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient {
|
||||
const { securityPluginSetup, actions, features } = this;
|
||||
const spaceId = this.getSpaceId(request);
|
||||
const authorization = new AlertsAuthorization({
|
||||
authorization: securityPluginSetup?.authz,
|
||||
request,
|
||||
getSpace: this.getSpace,
|
||||
alertTypeRegistry: this.alertTypeRegistry,
|
||||
features: features!,
|
||||
auditLogger: new AlertsAuthorizationAuditLogger(
|
||||
securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID)
|
||||
),
|
||||
});
|
||||
|
||||
return new AlertsClient({
|
||||
spaceId,
|
||||
logger: this.logger,
|
||||
taskManager: this.taskManager,
|
||||
alertTypeRegistry: this.alertTypeRegistry,
|
||||
savedObjectsClient,
|
||||
unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
}),
|
||||
authorization,
|
||||
actionsAuthorization: actions.getActionsAuthorizationWithRequest(request),
|
||||
namespace: this.spaceIdToNamespace(spaceId),
|
||||
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
|
||||
async getUserName() {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertsAuthorization } from './alerts_authorization';
|
||||
|
||||
type Schema = PublicMethodsOf<AlertsAuthorization>;
|
||||
export type AlertsAuthorizationMock = jest.Mocked<Schema>;
|
||||
|
||||
const createAlertsAuthorizationMock = () => {
|
||||
const mocked: AlertsAuthorizationMock = {
|
||||
ensureAuthorized: jest.fn(),
|
||||
filterByAlertTypeAuthorization: jest.fn(),
|
||||
getFindAuthorizationFilter: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const alertsAuthorizationMock: {
|
||||
create: () => AlertsAuthorizationMock;
|
||||
} = {
|
||||
create: createAlertsAuthorizationMock,
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,457 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { map, mapValues, remove, fromPairs, has } from 'lodash';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { ALERTS_FEATURE_ID } from '../../common';
|
||||
import { AlertTypeRegistry } from '../types';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { RegistryAlertType } from '../alert_type_registry';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../../features/server';
|
||||
import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger';
|
||||
import { Space } from '../../../spaces/server';
|
||||
|
||||
export enum ReadOperations {
|
||||
Get = 'get',
|
||||
GetAlertState = 'getAlertState',
|
||||
Find = 'find',
|
||||
}
|
||||
|
||||
export enum WriteOperations {
|
||||
Create = 'create',
|
||||
Delete = 'delete',
|
||||
Update = 'update',
|
||||
UpdateApiKey = 'updateApiKey',
|
||||
Enable = 'enable',
|
||||
Disable = 'disable',
|
||||
MuteAll = 'muteAll',
|
||||
UnmuteAll = 'unmuteAll',
|
||||
MuteInstance = 'muteInstance',
|
||||
UnmuteInstance = 'unmuteInstance',
|
||||
}
|
||||
|
||||
interface HasPrivileges {
|
||||
read: boolean;
|
||||
all: boolean;
|
||||
}
|
||||
type AuthorizedConsumers = Record<string, HasPrivileges>;
|
||||
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
|
||||
authorizedConsumers: AuthorizedConsumers;
|
||||
}
|
||||
|
||||
type IsAuthorizedAtProducerLevel = boolean;
|
||||
|
||||
export interface ConstructorOptions {
|
||||
alertTypeRegistry: AlertTypeRegistry;
|
||||
request: KibanaRequest;
|
||||
features: FeaturesPluginStart;
|
||||
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
auditLogger: AlertsAuthorizationAuditLogger;
|
||||
authorization?: SecurityPluginSetup['authz'];
|
||||
}
|
||||
|
||||
export class AlertsAuthorization {
|
||||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly request: KibanaRequest;
|
||||
private readonly authorization?: SecurityPluginSetup['authz'];
|
||||
private readonly auditLogger: AlertsAuthorizationAuditLogger;
|
||||
private readonly featuresIds: Promise<Set<string>>;
|
||||
private readonly allPossibleConsumers: Promise<AuthorizedConsumers>;
|
||||
|
||||
constructor({
|
||||
alertTypeRegistry,
|
||||
request,
|
||||
authorization,
|
||||
features,
|
||||
auditLogger,
|
||||
getSpace,
|
||||
}: ConstructorOptions) {
|
||||
this.request = request;
|
||||
this.authorization = authorization;
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
this.auditLogger = auditLogger;
|
||||
|
||||
this.featuresIds = getSpace(request)
|
||||
.then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? []))
|
||||
.then(
|
||||
(disabledFeatures) =>
|
||||
new Set(
|
||||
features
|
||||
.getFeatures()
|
||||
.filter(
|
||||
({ id, alerting }) =>
|
||||
// ignore features which are disabled in the user's space
|
||||
!disabledFeatures.has(id) &&
|
||||
// ignore features which don't grant privileges to alerting
|
||||
(alerting?.length ?? 0 > 0)
|
||||
)
|
||||
.map((feature) => feature.id)
|
||||
)
|
||||
)
|
||||
.catch(() => {
|
||||
// failing to fetch the space means the user is likely not privileged in the
|
||||
// active space at all, which means that their list of features should be empty
|
||||
return new Set();
|
||||
});
|
||||
|
||||
this.allPossibleConsumers = this.featuresIds.then((featuresIds) =>
|
||||
featuresIds.size
|
||||
? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], {
|
||||
read: true,
|
||||
all: true,
|
||||
})
|
||||
: {}
|
||||
);
|
||||
}
|
||||
|
||||
private shouldCheckAuthorization(): boolean {
|
||||
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
|
||||
}
|
||||
|
||||
public async ensureAuthorized(
|
||||
alertTypeId: string,
|
||||
consumer: string,
|
||||
operation: ReadOperations | WriteOperations
|
||||
) {
|
||||
const { authorization } = this;
|
||||
|
||||
const isAvailableConsumer = has(await this.allPossibleConsumers, consumer);
|
||||
if (authorization && this.shouldCheckAuthorization()) {
|
||||
const alertType = this.alertTypeRegistry.get(alertTypeId);
|
||||
const requiredPrivilegesByScope = {
|
||||
consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation),
|
||||
producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation),
|
||||
};
|
||||
|
||||
// We special case the Alerts Management `consumer` as we don't want to have to
|
||||
// manually authorize each alert type in the management UI
|
||||
const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID;
|
||||
|
||||
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
|
||||
const { hasAllRequested, username, privileges } = await checkPrivileges(
|
||||
shouldAuthorizeConsumer && consumer !== alertType.producer
|
||||
? [
|
||||
// check for access at consumer level
|
||||
requiredPrivilegesByScope.consumer,
|
||||
// check for access at producer level
|
||||
requiredPrivilegesByScope.producer,
|
||||
]
|
||||
: [
|
||||
// skip consumer privilege checks under `alerts` as all alert types can
|
||||
// be created under `alerts` if you have producer level privileges
|
||||
requiredPrivilegesByScope.producer,
|
||||
]
|
||||
);
|
||||
|
||||
if (!isAvailableConsumer) {
|
||||
/**
|
||||
* Under most circumstances this would have been caught by `checkPrivileges` as
|
||||
* a user can't have Privileges to an unknown consumer, but super users
|
||||
* don't actually get "privilege checked" so the made up consumer *will* return
|
||||
* as Privileged.
|
||||
* This check will ensure we don't accidentally let these through
|
||||
*/
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
ScopeType.Consumer,
|
||||
consumer,
|
||||
operation
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAllRequested) {
|
||||
this.auditLogger.alertsAuthorizationSuccess(
|
||||
username,
|
||||
alertTypeId,
|
||||
ScopeType.Consumer,
|
||||
consumer,
|
||||
operation
|
||||
);
|
||||
} else {
|
||||
const authorizedPrivileges = map(
|
||||
privileges.filter((privilege) => privilege.authorized),
|
||||
'privilege'
|
||||
);
|
||||
const unauthorizedScopes = mapValues(
|
||||
requiredPrivilegesByScope,
|
||||
(privilege) => !authorizedPrivileges.includes(privilege)
|
||||
);
|
||||
|
||||
const [unauthorizedScopeType, unauthorizedScope] =
|
||||
shouldAuthorizeConsumer && unauthorizedScopes.consumer
|
||||
? [ScopeType.Consumer, consumer]
|
||||
: [ScopeType.Producer, alertType.producer];
|
||||
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
unauthorizedScopeType,
|
||||
unauthorizedScope,
|
||||
operation
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (!isAvailableConsumer) {
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.alertsAuthorizationFailure(
|
||||
'',
|
||||
alertTypeId,
|
||||
ScopeType.Consumer,
|
||||
consumer,
|
||||
operation
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getFindAuthorizationFilter(): Promise<{
|
||||
filter?: string;
|
||||
ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void;
|
||||
logSuccessfulAuthorization: () => void;
|
||||
}> {
|
||||
if (this.authorization && this.shouldCheckAuthorization()) {
|
||||
const {
|
||||
username,
|
||||
authorizedAlertTypes,
|
||||
} = await this.augmentAlertTypesWithAuthorization(this.alertTypeRegistry.list(), [
|
||||
ReadOperations.Find,
|
||||
]);
|
||||
|
||||
if (!authorizedAlertTypes.size) {
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find')
|
||||
);
|
||||
}
|
||||
|
||||
const authorizedAlertTypeIdsToConsumers = new Set<string>(
|
||||
[...authorizedAlertTypes].reduce<string[]>((alertTypeIdConsumerPairs, alertType) => {
|
||||
for (const consumer of Object.keys(alertType.authorizedConsumers)) {
|
||||
alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`);
|
||||
}
|
||||
return alertTypeIdConsumerPairs;
|
||||
}, [])
|
||||
);
|
||||
|
||||
const authorizedEntries: Map<string, Set<string>> = new Map();
|
||||
return {
|
||||
filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`,
|
||||
ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {
|
||||
if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) {
|
||||
throw Boom.forbidden(
|
||||
this.auditLogger.alertsAuthorizationFailure(
|
||||
username!,
|
||||
alertTypeId,
|
||||
ScopeType.Consumer,
|
||||
consumer,
|
||||
'find'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (authorizedEntries.has(alertTypeId)) {
|
||||
authorizedEntries.get(alertTypeId)!.add(consumer);
|
||||
} else {
|
||||
authorizedEntries.set(alertTypeId, new Set([consumer]));
|
||||
}
|
||||
}
|
||||
},
|
||||
logSuccessfulAuthorization: () => {
|
||||
if (authorizedEntries.size) {
|
||||
this.auditLogger.alertsBulkAuthorizationSuccess(
|
||||
username!,
|
||||
[...authorizedEntries.entries()].reduce<Array<[string, string]>>(
|
||||
(authorizedPairs, [alertTypeId, consumers]) => {
|
||||
for (const consumer of consumers) {
|
||||
authorizedPairs.push([alertTypeId, consumer]);
|
||||
}
|
||||
return authorizedPairs;
|
||||
},
|
||||
[]
|
||||
),
|
||||
ScopeType.Consumer,
|
||||
'find'
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {},
|
||||
logSuccessfulAuthorization: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
public async filterByAlertTypeAuthorization(
|
||||
alertTypes: Set<RegistryAlertType>,
|
||||
operations: Array<ReadOperations | WriteOperations>
|
||||
): Promise<Set<RegistryAlertTypeWithAuth>> {
|
||||
const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization(
|
||||
alertTypes,
|
||||
operations
|
||||
);
|
||||
return authorizedAlertTypes;
|
||||
}
|
||||
|
||||
private async augmentAlertTypesWithAuthorization(
|
||||
alertTypes: Set<RegistryAlertType>,
|
||||
operations: Array<ReadOperations | WriteOperations>
|
||||
): Promise<{
|
||||
username?: string;
|
||||
hasAllRequested: boolean;
|
||||
authorizedAlertTypes: Set<RegistryAlertTypeWithAuth>;
|
||||
}> {
|
||||
const featuresIds = await this.featuresIds;
|
||||
if (this.authorization && this.shouldCheckAuthorization()) {
|
||||
const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(
|
||||
this.request
|
||||
);
|
||||
|
||||
// add an empty `authorizedConsumers` array on each alertType
|
||||
const alertTypesWithAuthorization = this.augmentWithAuthorizedConsumers(alertTypes, {});
|
||||
|
||||
// map from privilege to alertType which we can refer back to when analyzing the result
|
||||
// of checkPrivileges
|
||||
const privilegeToAlertType = new Map<
|
||||
string,
|
||||
[RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel]
|
||||
>();
|
||||
// as we can't ask ES for the user's individual privileges we need to ask for each feature
|
||||
// and alertType in the system whether this user has this privilege
|
||||
for (const alertType of alertTypesWithAuthorization) {
|
||||
for (const feature of featuresIds) {
|
||||
for (const operation of operations) {
|
||||
privilegeToAlertType.set(
|
||||
this.authorization!.actions.alerting.get(alertType.id, feature, operation),
|
||||
[
|
||||
alertType,
|
||||
feature,
|
||||
hasPrivilegeByOperation(operation),
|
||||
alertType.producer === feature,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { username, hasAllRequested, privileges } = await checkPrivileges([
|
||||
...privilegeToAlertType.keys(),
|
||||
]);
|
||||
|
||||
return {
|
||||
username,
|
||||
hasAllRequested,
|
||||
authorizedAlertTypes: hasAllRequested
|
||||
? // has access to all features
|
||||
this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers)
|
||||
: // only has some of the required privileges
|
||||
privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => {
|
||||
if (authorized && privilegeToAlertType.has(privilege)) {
|
||||
const [
|
||||
alertType,
|
||||
feature,
|
||||
hasPrivileges,
|
||||
isAuthorizedAtProducerLevel,
|
||||
] = privilegeToAlertType.get(privilege)!;
|
||||
alertType.authorizedConsumers[feature] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
alertType.authorizedConsumers[feature]
|
||||
);
|
||||
|
||||
if (isAuthorizedAtProducerLevel) {
|
||||
// granting privileges under the producer automatically authorized the Alerts Management UI as well
|
||||
alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
alertType.authorizedConsumers[ALERTS_FEATURE_ID]
|
||||
);
|
||||
}
|
||||
authorizedAlertTypes.add(alertType);
|
||||
}
|
||||
return authorizedAlertTypes;
|
||||
}, new Set<RegistryAlertTypeWithAuth>()),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
hasAllRequested: true,
|
||||
authorizedAlertTypes: this.augmentWithAuthorizedConsumers(
|
||||
new Set([...alertTypes].filter((alertType) => featuresIds.has(alertType.producer))),
|
||||
await this.allPossibleConsumers
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private augmentWithAuthorizedConsumers(
|
||||
alertTypes: Set<RegistryAlertType>,
|
||||
authorizedConsumers: AuthorizedConsumers
|
||||
): Set<RegistryAlertTypeWithAuth> {
|
||||
return new Set(
|
||||
Array.from(alertTypes).map((alertType) => ({
|
||||
...alertType,
|
||||
authorizedConsumers: { ...authorizedConsumers },
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private asFiltersByAlertTypeAndConsumer(alertTypes: Set<RegistryAlertTypeWithAuth>): string[] {
|
||||
return Array.from(alertTypes).reduce<string[]>((filters, { id, authorizedConsumers }) => {
|
||||
ensureFieldIsSafeForQuery('alertTypeId', id);
|
||||
filters.push(
|
||||
`(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${Object.keys(
|
||||
authorizedConsumers
|
||||
)
|
||||
.map((consumer) => {
|
||||
ensureFieldIsSafeForQuery('alertTypeId', id);
|
||||
return consumer;
|
||||
})
|
||||
.join(' or ')}))`
|
||||
);
|
||||
return filters;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureFieldIsSafeForQuery(field: string, value: string): boolean {
|
||||
const invalid = value.match(/([>=<\*:()]+|\s+)/g);
|
||||
if (invalid) {
|
||||
const whitespace = remove(invalid, (chars) => chars.trim().length === 0);
|
||||
const errors = [];
|
||||
if (whitespace.length) {
|
||||
errors.push(`whitespace`);
|
||||
}
|
||||
if (invalid.length) {
|
||||
errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`);
|
||||
}
|
||||
throw new Error(`expected ${field} not to include ${errors.join(' and ')}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges {
|
||||
return {
|
||||
read: (left.read || right?.read) ?? false,
|
||||
all: (left.all || right?.all) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): HasPrivileges {
|
||||
const read = Object.values(ReadOperations).includes((operation as unknown) as ReadOperations);
|
||||
const all = Object.values(WriteOperations).includes((operation as unknown) as WriteOperations);
|
||||
return {
|
||||
read: read || all,
|
||||
all,
|
||||
};
|
||||
}
|
||||
|
||||
function asAuthorizedConsumers(
|
||||
consumers: string[],
|
||||
hasPrivileges: HasPrivileges
|
||||
): AuthorizedConsumers {
|
||||
return fromPairs(consumers.map((feature) => [feature, hasPrivileges]));
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertsAuthorizationAuditLogger } from './audit_logger';
|
||||
|
||||
const createAlertsAuthorizationAuditLoggerMock = () => {
|
||||
const mocked = ({
|
||||
getAuthorizationMessage: jest.fn(),
|
||||
alertsAuthorizationFailure: jest.fn(),
|
||||
alertsUnscopedAuthorizationFailure: jest.fn(),
|
||||
alertsAuthorizationSuccess: jest.fn(),
|
||||
alertsBulkAuthorizationSuccess: jest.fn(),
|
||||
} as unknown) as jest.Mocked<AlertsAuthorizationAuditLogger>;
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const alertsAuthorizationAuditLoggerMock: {
|
||||
create: () => jest.Mocked<AlertsAuthorizationAuditLogger>;
|
||||
} = {
|
||||
create: createAlertsAuthorizationAuditLoggerMock,
|
||||
};
|
311
x-pack/plugins/alerts/server/authorization/audit_logger.test.ts
Normal file
311
x-pack/plugins/alerts/server/authorization/audit_logger.test.ts
Normal file
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger';
|
||||
|
||||
const createMockAuditLogger = () => {
|
||||
return {
|
||||
log: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
describe(`#constructor`, () => {
|
||||
test('initializes a noop auditLogger if security logger is unavailable', () => {
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(undefined);
|
||||
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Consumer;
|
||||
const scope = 'myApp';
|
||||
const operation = 'create';
|
||||
expect(() => {
|
||||
alertsAuditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationSuccess(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#alertsUnscopedAuthorizationFailure`, () => {
|
||||
test('logs auth failure of operation', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_unscoped_authorization_failure",
|
||||
"foo-user Unauthorized to create any alert types",
|
||||
Object {
|
||||
"operation": "create",
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs auth failure with producer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Producer;
|
||||
const scope = 'myOtherApp';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_failure",
|
||||
"foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
|
||||
Object {
|
||||
"alertTypeId": "alert-type-id",
|
||||
"operation": "create",
|
||||
"scope": "myOtherApp",
|
||||
"scopeType": 1,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#alertsAuthorizationFailure`, () => {
|
||||
test('logs auth failure with consumer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Consumer;
|
||||
const scope = 'myApp';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_failure",
|
||||
"foo-user Unauthorized to create a \\"alert-type-id\\" alert for \\"myApp\\"",
|
||||
Object {
|
||||
"alertTypeId": "alert-type-id",
|
||||
"operation": "create",
|
||||
"scope": "myApp",
|
||||
"scopeType": 0,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs auth failure with producer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Producer;
|
||||
const scope = 'myOtherApp';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationFailure(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_failure",
|
||||
"foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
|
||||
Object {
|
||||
"alertTypeId": "alert-type-id",
|
||||
"operation": "create",
|
||||
"scope": "myOtherApp",
|
||||
"scopeType": 1,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#alertsBulkAuthorizationSuccess`, () => {
|
||||
test('logs auth success with consumer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const scopeType = ScopeType.Consumer;
|
||||
const authorizedEntries: Array<[string, string]> = [
|
||||
['alert-type-id', 'myApp'],
|
||||
['other-alert-type-id', 'myOtherApp'],
|
||||
];
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsBulkAuthorizationSuccess(
|
||||
username,
|
||||
authorizedEntries,
|
||||
scopeType,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_success",
|
||||
"foo-user Authorized to create: \\"alert-type-id\\" alert for \\"myApp\\", \\"other-alert-type-id\\" alert for \\"myOtherApp\\"",
|
||||
Object {
|
||||
"authorizedEntries": Array [
|
||||
Array [
|
||||
"alert-type-id",
|
||||
"myApp",
|
||||
],
|
||||
Array [
|
||||
"other-alert-type-id",
|
||||
"myOtherApp",
|
||||
],
|
||||
],
|
||||
"operation": "create",
|
||||
"scopeType": 0,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs auth success with producer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const scopeType = ScopeType.Producer;
|
||||
const authorizedEntries: Array<[string, string]> = [
|
||||
['alert-type-id', 'myApp'],
|
||||
['other-alert-type-id', 'myOtherApp'],
|
||||
];
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsBulkAuthorizationSuccess(
|
||||
username,
|
||||
authorizedEntries,
|
||||
scopeType,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_success",
|
||||
"foo-user Authorized to create: \\"alert-type-id\\" alert by \\"myApp\\", \\"other-alert-type-id\\" alert by \\"myOtherApp\\"",
|
||||
Object {
|
||||
"authorizedEntries": Array [
|
||||
Array [
|
||||
"alert-type-id",
|
||||
"myApp",
|
||||
],
|
||||
Array [
|
||||
"other-alert-type-id",
|
||||
"myOtherApp",
|
||||
],
|
||||
],
|
||||
"operation": "create",
|
||||
"scopeType": 1,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#savedObjectsAuthorizationSuccess`, () => {
|
||||
test('logs auth success with consumer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Consumer;
|
||||
const scope = 'myApp';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationSuccess(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_success",
|
||||
"foo-user Authorized to create a \\"alert-type-id\\" alert for \\"myApp\\"",
|
||||
Object {
|
||||
"alertTypeId": "alert-type-id",
|
||||
"operation": "create",
|
||||
"scope": "myApp",
|
||||
"scopeType": 0,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs auth success with producer scope', () => {
|
||||
const auditLogger = createMockAuditLogger();
|
||||
const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
|
||||
const username = 'foo-user';
|
||||
const alertTypeId = 'alert-type-id';
|
||||
const scopeType = ScopeType.Producer;
|
||||
const scope = 'myOtherApp';
|
||||
const operation = 'create';
|
||||
|
||||
alertsAuditLogger.alertsAuthorizationSuccess(
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
|
||||
expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerts_authorization_success",
|
||||
"foo-user Authorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
|
||||
Object {
|
||||
"alertTypeId": "alert-type-id",
|
||||
"operation": "create",
|
||||
"scope": "myOtherApp",
|
||||
"scopeType": 1,
|
||||
"username": "foo-user",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
117
x-pack/plugins/alerts/server/authorization/audit_logger.ts
Normal file
117
x-pack/plugins/alerts/server/authorization/audit_logger.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
|
||||
export enum ScopeType {
|
||||
Consumer,
|
||||
Producer,
|
||||
}
|
||||
|
||||
export enum AuthorizationResult {
|
||||
Unauthorized = 'Unauthorized',
|
||||
Authorized = 'Authorized',
|
||||
}
|
||||
|
||||
export class AlertsAuthorizationAuditLogger {
|
||||
private readonly auditLogger: AuditLogger;
|
||||
|
||||
constructor(auditLogger: AuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
public getAuthorizationMessage(
|
||||
authorizationResult: AuthorizationResult,
|
||||
alertTypeId: string,
|
||||
scopeType: ScopeType,
|
||||
scope: string,
|
||||
operation: string
|
||||
): string {
|
||||
return `${authorizationResult} to ${operation} a "${alertTypeId}" alert ${
|
||||
scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"`
|
||||
}`;
|
||||
}
|
||||
|
||||
public alertsAuthorizationFailure(
|
||||
username: string,
|
||||
alertTypeId: string,
|
||||
scopeType: ScopeType,
|
||||
scope: string,
|
||||
operation: string
|
||||
): string {
|
||||
const message = this.getAuthorizationMessage(
|
||||
AuthorizationResult.Unauthorized,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, {
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
public alertsUnscopedAuthorizationFailure(username: string, operation: string): string {
|
||||
const message = `Unauthorized to ${operation} any alert types`;
|
||||
this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, {
|
||||
username,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
public alertsAuthorizationSuccess(
|
||||
username: string,
|
||||
alertTypeId: string,
|
||||
scopeType: ScopeType,
|
||||
scope: string,
|
||||
operation: string
|
||||
): string {
|
||||
const message = this.getAuthorizationMessage(
|
||||
AuthorizationResult.Authorized,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation
|
||||
);
|
||||
this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, {
|
||||
username,
|
||||
alertTypeId,
|
||||
scopeType,
|
||||
scope,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
public alertsBulkAuthorizationSuccess(
|
||||
username: string,
|
||||
authorizedEntries: Array<[string, string]>,
|
||||
scopeType: ScopeType,
|
||||
operation: string
|
||||
): string {
|
||||
const message = `${AuthorizationResult.Authorized} to ${operation}: ${authorizedEntries
|
||||
.map(
|
||||
([alertTypeId, scope]) =>
|
||||
`"${alertTypeId}" alert ${
|
||||
scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"`
|
||||
}`
|
||||
)
|
||||
.join(', ')}`;
|
||||
this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, {
|
||||
username,
|
||||
scopeType,
|
||||
authorizedEntries,
|
||||
operation,
|
||||
});
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ export {
|
|||
PartialAlert,
|
||||
} from './types';
|
||||
export { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
export { FindOptions, FindResult } from './alerts_client';
|
||||
export { FindResult } from './alerts_client';
|
||||
export { AlertInstance } from './alert_instance';
|
||||
export { parseDuration } from './lib';
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ test('should return passed in params when validation not defined', () => {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
async executor() {},
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
},
|
||||
{
|
||||
foo: true,
|
||||
|
@ -48,7 +48,7 @@ test('should validate and apply defaults when params is valid', () => {
|
|||
}),
|
||||
},
|
||||
async executor() {},
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
},
|
||||
{ param1: 'value' }
|
||||
);
|
||||
|
@ -77,7 +77,7 @@ test('should validate and throw error when params is invalid', () => {
|
|||
}),
|
||||
},
|
||||
async executor() {},
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
|
|
@ -11,6 +11,8 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/
|
|||
import { taskManagerMock } from '../../task_manager/server/mocks';
|
||||
import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock';
|
||||
import { KibanaRequest, CoreSetup } from 'kibana/server';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { Feature } from '../../features/server';
|
||||
|
||||
describe('Alerting Plugin', () => {
|
||||
describe('setup()', () => {
|
||||
|
@ -80,8 +82,10 @@ describe('Alerting Plugin', () => {
|
|||
actions: {
|
||||
execute: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn(),
|
||||
getActionsAuthorizationWithRequest: jest.fn(),
|
||||
},
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
features: mockFeatures(),
|
||||
} as unknown) as AlertingPluginsStart
|
||||
);
|
||||
|
||||
|
@ -124,9 +128,11 @@ describe('Alerting Plugin', () => {
|
|||
actions: {
|
||||
execute: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn(),
|
||||
getActionsAuthorizationWithRequest: jest.fn(),
|
||||
},
|
||||
spaces: () => null,
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
features: mockFeatures(),
|
||||
} as unknown) as AlertingPluginsStart
|
||||
);
|
||||
|
||||
|
@ -150,3 +156,31 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockFeatures() {
|
||||
const features = featuresPluginMock.createSetup();
|
||||
features.getFeatures.mockReturnValue([
|
||||
new Feature({
|
||||
id: 'appName',
|
||||
name: 'appName',
|
||||
app: [],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return features;
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import { Services } from './types';
|
|||
import { registerAlertsUsageCollector } from './usage';
|
||||
import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task';
|
||||
import { IEventLogger, IEventLogService } from '../../event_log/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
|
||||
const EVENT_LOG_PROVIDER = 'alerting';
|
||||
|
@ -90,6 +91,7 @@ export interface AlertingPluginsStart {
|
|||
actions: ActionsPluginStartContract;
|
||||
taskManager: TaskManagerStartContract;
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
features: FeaturesPluginStart;
|
||||
}
|
||||
|
||||
export class AlertingPlugin {
|
||||
|
@ -216,12 +218,26 @@ export class AlertingPlugin {
|
|||
getSpaceId(request: KibanaRequest) {
|
||||
return spaces?.getSpaceId(request);
|
||||
},
|
||||
async getSpace(request: KibanaRequest) {
|
||||
return spaces?.getActiveSpace(request);
|
||||
},
|
||||
actions: plugins.actions,
|
||||
features: plugins.features,
|
||||
});
|
||||
|
||||
const getAlertsClientWithRequest = (request: KibanaRequest) => {
|
||||
if (isESOUsingEphemeralEncryptionKey === true) {
|
||||
throw new Error(
|
||||
`Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
|
||||
);
|
||||
}
|
||||
return alertsClientFactory!.create(request, core.savedObjects);
|
||||
};
|
||||
|
||||
taskRunnerFactory.initialize({
|
||||
logger,
|
||||
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
|
||||
getAlertsClientWithRequest,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
actionsPlugin: plugins.actions,
|
||||
encryptedSavedObjectsClient,
|
||||
|
@ -233,18 +249,7 @@ export class AlertingPlugin {
|
|||
|
||||
return {
|
||||
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
|
||||
// Ability to get an alerts client from legacy code
|
||||
getAlertsClientWithRequest: (request: KibanaRequest) => {
|
||||
if (isESOUsingEphemeralEncryptionKey === true) {
|
||||
throw new Error(
|
||||
`Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
|
||||
);
|
||||
}
|
||||
return alertsClientFactory!.create(
|
||||
request,
|
||||
this.getScopedClientWithAlertSavedObjectType(core.savedObjects, request)
|
||||
);
|
||||
},
|
||||
getAlertsClientWithRequest,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -252,14 +257,11 @@ export class AlertingPlugin {
|
|||
core: CoreSetup
|
||||
): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'alerting'> => {
|
||||
const { alertTypeRegistry, alertsClientFactory } = this;
|
||||
return async (context, request) => {
|
||||
return async function alertsRouteHandlerContext(context, request) {
|
||||
const [{ savedObjects }] = await core.getStartServices();
|
||||
return {
|
||||
getAlertsClient: () => {
|
||||
return alertsClientFactory!.create(
|
||||
request,
|
||||
this.getScopedClientWithAlertSavedObjectType(savedObjects, request)
|
||||
);
|
||||
return alertsClientFactory!.create(request, savedObjects);
|
||||
},
|
||||
listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!),
|
||||
};
|
||||
|
|
|
@ -75,13 +75,6 @@ describe('createAlertRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.create.mockResolvedValueOnce(createResult);
|
||||
|
||||
|
|
|
@ -47,9 +47,6 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function (
|
||||
|
|
|
@ -30,13 +30,6 @@ describe('deleteAlertRoute', () => {
|
|||
const [config, handler] = router.delete.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.delete.mockResolvedValueOnce({});
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -30,13 +30,6 @@ describe('disableAlertRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.disable.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) =
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -29,13 +29,6 @@ describe('enableAlertRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.enable.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -28,9 +28,6 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function (
|
||||
|
|
|
@ -31,13 +31,6 @@ describe('findAlertRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const findResult = {
|
||||
page: 1,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { LicenseState } from '../lib/license_state';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { BASE_ALERT_API_PATH } from '../../common';
|
||||
import { renameKeys } from './lib/rename_keys';
|
||||
import { FindOptions } from '..';
|
||||
import { FindOptions } from '../alerts_client';
|
||||
|
||||
// config definition
|
||||
const querySchema = schema.object({
|
||||
|
@ -50,9 +50,6 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => {
|
|||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -61,13 +61,6 @@ describe('getAlertRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlert);
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => {
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -48,13 +48,6 @@ describe('getAlertStateRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState);
|
||||
|
||||
|
@ -91,13 +84,6 @@ describe('getAlertStateRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.getAlertState.mockResolvedValueOnce(undefined);
|
||||
|
||||
|
@ -134,13 +120,6 @@ describe('getAlertStateRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.getAlertState = jest
|
||||
.fn()
|
||||
|
|
|
@ -27,9 +27,6 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState)
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -9,6 +9,9 @@ import { httpServiceMock } from 'src/core/server/mocks';
|
|||
import { mockLicenseState } from '../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { alertsClientMock } from '../alerts_client.mock';
|
||||
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
|
@ -28,13 +31,6 @@ describe('listAlertTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
|
@ -47,12 +43,17 @@ describe('listAlertTypesRoute', () => {
|
|||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
actionVariables: [],
|
||||
authorizedConsumers: {},
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [],
|
||||
},
|
||||
producer: 'test',
|
||||
},
|
||||
];
|
||||
alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments({ alertsClient }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -64,7 +65,11 @@ describe('listAlertTypesRoute', () => {
|
|||
"name": "Default",
|
||||
},
|
||||
],
|
||||
"actionVariables": Array [],
|
||||
"actionVariables": Object {
|
||||
"context": Array [],
|
||||
"state": Array [],
|
||||
},
|
||||
"authorizedConsumers": Object {},
|
||||
"defaultActionGroupId": "default",
|
||||
"id": "1",
|
||||
"name": "name",
|
||||
|
@ -74,7 +79,7 @@ describe('listAlertTypesRoute', () => {
|
|||
}
|
||||
`);
|
||||
|
||||
expect(context.alerting!.listTypes).toHaveBeenCalledTimes(1);
|
||||
expect(alertsClient.listAlertTypes).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: listTypes,
|
||||
|
@ -90,19 +95,11 @@ describe('listAlertTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
|
@ -110,13 +107,19 @@ describe('listAlertTypesRoute', () => {
|
|||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
actionVariables: [],
|
||||
producer: 'alerting',
|
||||
authorizedConsumers: {},
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [],
|
||||
},
|
||||
producer: 'alerts',
|
||||
},
|
||||
];
|
||||
|
||||
alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ listTypes },
|
||||
{ alertsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
|
@ -141,13 +144,6 @@ describe('listAlertTypesRoute', () => {
|
|||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-read",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
|
@ -160,13 +156,19 @@ describe('listAlertTypesRoute', () => {
|
|||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
actionVariables: [],
|
||||
producer: 'alerting',
|
||||
authorizedConsumers: {},
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [],
|
||||
},
|
||||
producer: 'alerts',
|
||||
},
|
||||
];
|
||||
|
||||
alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ listTypes },
|
||||
{ alertsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
|
|
|
@ -20,9 +20,6 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState)
|
|||
{
|
||||
path: `${BASE_ALERT_API_PATH}/list_alert_types`,
|
||||
validate: {},
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
@ -34,7 +31,7 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState)
|
|||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
return res.ok({
|
||||
body: context.alerting.listTypes(),
|
||||
body: Array.from(await context.alerting.getAlertsClient().listAlertTypes()),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -29,13 +29,6 @@ describe('muteAllAlertRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.muteAll.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) =
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -31,13 +31,6 @@ describe('muteAlertInstanceRoute', () => {
|
|||
expect(config.path).toMatchInlineSnapshot(
|
||||
`"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"`
|
||||
);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.muteInstance.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -30,9 +30,6 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -28,13 +28,6 @@ describe('unmuteAllAlertRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.unmuteAll.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -27,9 +27,6 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState)
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -31,13 +31,6 @@ describe('unmuteAlertInstanceRoute', () => {
|
|||
expect(config.path).toMatchInlineSnapshot(
|
||||
`"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"`
|
||||
);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.unmuteInstance.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -28,9 +28,6 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function (
|
||||
context: RequestHandlerContext,
|
||||
|
|
|
@ -52,13 +52,6 @@ describe('updateAlertRoute', () => {
|
|||
const [config, handler] = router.put.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.update.mockResolvedValueOnce(mockedResponse);
|
||||
|
||||
|
|
|
@ -49,9 +49,6 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
body: bodySchema,
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function (
|
||||
|
|
|
@ -29,13 +29,6 @@ describe('updateApiKeyRoute', () => {
|
|||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`);
|
||||
expect(config.options).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"tags": Array [
|
||||
"access:alerting-all",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
alertsClient.updateApiKey.mockResolvedValueOnce();
|
||||
|
||||
|
|
|
@ -28,9 +28,6 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) =
|
|||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function (
|
||||
|
|
|
@ -47,6 +47,40 @@ describe('7.9.0', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('7.10.0', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
encryptedSavedObjectsSetup.createMigration.mockImplementation(
|
||||
(shouldMigrateWhenPredicate, migration) => migration
|
||||
);
|
||||
});
|
||||
|
||||
test('changes nothing on alerts by other plugins', () => {
|
||||
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
|
||||
const alert = getMockData({});
|
||||
expect(migration710(alert, { log })).toMatchObject(alert);
|
||||
|
||||
expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
test('migrates the consumer for metrics', () => {
|
||||
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
|
||||
const alert = getMockData({
|
||||
consumer: 'metrics',
|
||||
});
|
||||
expect(migration710(alert, { log })).toMatchObject({
|
||||
...alert,
|
||||
attributes: {
|
||||
...alert.attributes,
|
||||
consumer: 'infrastructure',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getMockData(
|
||||
overwrites: Record<string, unknown> = {}
|
||||
): SavedObjectUnsanitizedDoc<RawAlert> {
|
||||
|
|
|
@ -15,23 +15,27 @@ export function getMigrations(
|
|||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
): SavedObjectMigrationMap {
|
||||
return {
|
||||
'7.9.0': changeAlertingConsumer(encryptedSavedObjects),
|
||||
/**
|
||||
* In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts`
|
||||
* prior to that we were using `alerting` and we need to keep these in sync
|
||||
*/
|
||||
'7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'),
|
||||
/**
|
||||
* In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure`
|
||||
* prior to that we were using `metrics` and we need to keep these in sync
|
||||
*/
|
||||
'7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts`
|
||||
* prior to that we were using `alerting` and we need to keep these in sync
|
||||
*/
|
||||
function changeAlertingConsumer(
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
|
||||
from: string,
|
||||
to: string
|
||||
): SavedObjectMigrationFn<RawAlert, RawAlert> {
|
||||
const consumerMigration = new Map<string, string>();
|
||||
consumerMigration.set('alerting', 'alerts');
|
||||
|
||||
return encryptedSavedObjects.createMigration<RawAlert, RawAlert>(
|
||||
function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc<RawAlert> {
|
||||
return consumerMigration.has(doc.attributes.consumer);
|
||||
return doc.attributes.consumer === from;
|
||||
},
|
||||
(doc: SavedObjectUnsanitizedDoc<RawAlert>): SavedObjectUnsanitizedDoc<RawAlert> => {
|
||||
const {
|
||||
|
@ -41,7 +45,7 @@ function changeAlertingConsumer(
|
|||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
consumer: consumerMigration.get(consumer) ?? consumer,
|
||||
consumer: consumer === from ? to : consumer,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ const alertType: AlertType = {
|
|||
],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
|
|
@ -14,7 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
|
|||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
|
||||
import { actionsMock, actionsClientMock } from '../../../actions/server/mocks';
|
||||
import { alertsMock } from '../mocks';
|
||||
import { alertsMock, alertsClientMock } from '../mocks';
|
||||
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
|
||||
import { IEventLogger } from '../../../event_log/server';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
|
||||
|
@ -25,7 +25,7 @@ const alertType = {
|
|||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
executor: jest.fn(),
|
||||
producer: 'alerting',
|
||||
producer: 'alerts',
|
||||
};
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
|
||||
|
@ -56,8 +56,8 @@ describe('Task Runner', () => {
|
|||
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
|
||||
const services = alertsMock.createAlertServices();
|
||||
const savedObjectsClient = services.savedObjectsClient;
|
||||
const actionsClient = actionsClientMock.create();
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> & {
|
||||
actionsPlugin: jest.Mocked<ActionsPluginStart>;
|
||||
|
@ -65,6 +65,7 @@ describe('Task Runner', () => {
|
|||
} = {
|
||||
getServices: jest.fn().mockReturnValue(services),
|
||||
actionsPlugin: actionsMock.createStart(),
|
||||
getAlertsClientWithRequest: jest.fn().mockReturnValue(alertsClient),
|
||||
encryptedSavedObjectsClient,
|
||||
logger: loggingSystemMock.create().get(),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
@ -74,34 +75,31 @@ describe('Task Runner', () => {
|
|||
|
||||
const mockedAlertTypeSavedObject = {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
name: 'alert-name',
|
||||
tags: ['alert-', '-tags'],
|
||||
createdBy: 'alert-creator',
|
||||
updatedBy: 'alert-updater',
|
||||
mutedInstanceIds: [],
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
consumer: 'bar',
|
||||
createdAt: new Date('2019-02-12T21:01:22.479Z'),
|
||||
updatedAt: new Date('2019-02-12T21:01:22.479Z'),
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
enabled: true,
|
||||
alertTypeId: '123',
|
||||
apiKeyOwner: 'elastic',
|
||||
schedule: { interval: '10s' },
|
||||
name: 'alert-name',
|
||||
tags: ['alert-', '-tags'],
|
||||
createdBy: 'alert-creator',
|
||||
updatedBy: 'alert-updater',
|
||||
mutedInstanceIds: [],
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
references: [
|
||||
actions: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
group: 'default',
|
||||
id: '1',
|
||||
actionTypeId: 'action',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -109,6 +107,7 @@ describe('Task Runner', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
|
||||
taskRunnerFactoryInitializerParams.getAlertsClientWithRequest.mockReturnValue(alertsClient);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue(
|
||||
actionsClient
|
||||
);
|
||||
|
@ -126,7 +125,7 @@ describe('Task Runner', () => {
|
|||
},
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -200,7 +199,7 @@ describe('Task Runner', () => {
|
|||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -285,7 +284,7 @@ describe('Task Runner', () => {
|
|||
],
|
||||
},
|
||||
message:
|
||||
"alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1",
|
||||
"alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1",
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -302,7 +301,7 @@ describe('Task Runner', () => {
|
|||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -412,7 +411,7 @@ describe('Task Runner', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
"message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1",
|
||||
"message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1",
|
||||
},
|
||||
],
|
||||
]
|
||||
|
@ -439,7 +438,7 @@ describe('Task Runner', () => {
|
|||
},
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -526,7 +525,7 @@ describe('Task Runner', () => {
|
|||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -548,44 +547,13 @@ describe('Task Runner', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('throws error if reference not found', async () => {
|
||||
const taskRunner = new TaskRunner(
|
||||
alertType,
|
||||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
...mockedAlertTypeSavedObject,
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
expect(await taskRunner.run()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"runAt": 1970-01-01T00:00:10.000Z,
|
||||
"state": Object {
|
||||
"previousStartedAt": 1970-01-01T00:00:00.000Z,
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith(
|
||||
`Executing Alert \"1\" has resulted in Error: Action reference \"action_0\" not found in alert id: 1`
|
||||
);
|
||||
});
|
||||
|
||||
test('uses API key when provided', async () => {
|
||||
const taskRunner = new TaskRunner(
|
||||
alertType,
|
||||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -621,7 +589,7 @@ describe('Task Runner', () => {
|
|||
mockedTaskInstance,
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -660,7 +628,7 @@ describe('Task Runner', () => {
|
|||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -722,7 +690,7 @@ describe('Task Runner', () => {
|
|||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
|
||||
const runnerResult = await taskRunner.run();
|
||||
|
||||
|
@ -747,7 +715,7 @@ describe('Task Runner', () => {
|
|||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
|
@ -770,7 +738,7 @@ describe('Task Runner', () => {
|
|||
});
|
||||
|
||||
test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => {
|
||||
savedObjectsClient.get.mockImplementation(() => {
|
||||
alertsClient.get.mockImplementation(() => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
|
@ -802,7 +770,7 @@ describe('Task Runner', () => {
|
|||
});
|
||||
|
||||
test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => {
|
||||
savedObjectsClient.get.mockImplementation(() => {
|
||||
alertsClient.get.mockImplementation(() => {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1');
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { pickBy, mapValues, omit, without } from 'lodash';
|
||||
import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { Logger, KibanaRequest } from '../../../../../src/core/server';
|
||||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { ConcreteTaskInstance } from '../../../task_manager/server';
|
||||
import { createExecutionHandler } from './create_execution_handler';
|
||||
|
@ -17,15 +17,18 @@ import {
|
|||
RawAlert,
|
||||
IntervalSchedule,
|
||||
Services,
|
||||
AlertInfoParams,
|
||||
AlertTaskState,
|
||||
RawAlertInstance,
|
||||
AlertTaskState,
|
||||
Alert,
|
||||
AlertExecutorOptions,
|
||||
SanitizedAlert,
|
||||
} from '../types';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
|
||||
import { EVENT_LOG_ACTIONS } from '../plugin';
|
||||
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
|
||||
import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error';
|
||||
import { AlertsClient } from '../alerts_client';
|
||||
|
||||
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
|
||||
|
||||
|
@ -93,8 +96,12 @@ export class TaskRunner {
|
|||
} as unknown) as KibanaRequest;
|
||||
}
|
||||
|
||||
async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) {
|
||||
return this.context.getServices(this.getFakeKibanaRequest(spaceId, apiKey));
|
||||
private getServicesWithSpaceLevelPermissions(
|
||||
spaceId: string,
|
||||
apiKey: string | null
|
||||
): [Services, PublicMethodsOf<AlertsClient>] {
|
||||
const request = this.getFakeKibanaRequest(spaceId, apiKey);
|
||||
return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)];
|
||||
}
|
||||
|
||||
private getExecutionHandler(
|
||||
|
@ -103,21 +110,8 @@ export class TaskRunner {
|
|||
tags: string[] | undefined,
|
||||
spaceId: string,
|
||||
apiKey: string | null,
|
||||
actions: RawAlert['actions'],
|
||||
references: SavedObject['references']
|
||||
actions: Alert['actions']
|
||||
) {
|
||||
// Inject ids into actions
|
||||
const actionsWithIds = actions.map((action) => {
|
||||
const actionReference = references.find((obj) => obj.name === action.actionRef);
|
||||
if (!actionReference) {
|
||||
throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
id: actionReference.id,
|
||||
};
|
||||
});
|
||||
|
||||
return createExecutionHandler({
|
||||
alertId,
|
||||
alertName,
|
||||
|
@ -125,7 +119,7 @@ export class TaskRunner {
|
|||
logger: this.logger,
|
||||
actionsPlugin: this.context.actionsPlugin,
|
||||
apiKey,
|
||||
actions: actionsWithIds,
|
||||
actions,
|
||||
spaceId,
|
||||
alertType: this.alertType,
|
||||
eventLogger: this.context.eventLogger,
|
||||
|
@ -146,20 +140,12 @@ export class TaskRunner {
|
|||
|
||||
async executeAlertInstances(
|
||||
services: Services,
|
||||
alertInfoParams: AlertInfoParams,
|
||||
alert: SanitizedAlert,
|
||||
params: AlertExecutorOptions['params'],
|
||||
executionHandler: ReturnType<typeof createExecutionHandler>,
|
||||
spaceId: string
|
||||
): Promise<AlertTaskState> {
|
||||
const {
|
||||
params,
|
||||
throttle,
|
||||
muteAll,
|
||||
mutedInstanceIds,
|
||||
name,
|
||||
tags,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
} = alertInfoParams;
|
||||
const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert;
|
||||
const {
|
||||
params: { alertId },
|
||||
state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt },
|
||||
|
@ -262,33 +248,22 @@ export class TaskRunner {
|
|||
};
|
||||
}
|
||||
|
||||
async validateAndExecuteAlert(
|
||||
services: Services,
|
||||
apiKey: string | null,
|
||||
attributes: RawAlert,
|
||||
references: SavedObject['references']
|
||||
) {
|
||||
async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) {
|
||||
const {
|
||||
params: { alertId, spaceId },
|
||||
} = this.taskInstance;
|
||||
|
||||
// Validate
|
||||
const params = validateAlertTypeParams(this.alertType, attributes.params);
|
||||
const validatedParams = validateAlertTypeParams(this.alertType, alert.params);
|
||||
const executionHandler = this.getExecutionHandler(
|
||||
alertId,
|
||||
attributes.name,
|
||||
attributes.tags,
|
||||
alert.name,
|
||||
alert.tags,
|
||||
spaceId,
|
||||
apiKey,
|
||||
attributes.actions,
|
||||
references
|
||||
);
|
||||
return this.executeAlertInstances(
|
||||
services,
|
||||
{ ...attributes, params },
|
||||
executionHandler,
|
||||
spaceId
|
||||
alert.actions
|
||||
);
|
||||
return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId);
|
||||
}
|
||||
|
||||
async loadAlertAttributesAndRun(): Promise<Resultable<AlertTaskRunResult, Error>> {
|
||||
|
@ -297,17 +272,17 @@ export class TaskRunner {
|
|||
} = this.taskInstance;
|
||||
|
||||
const apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
|
||||
const services = await this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
|
||||
const [services, alertsClient] = await this.getServicesWithSpaceLevelPermissions(
|
||||
spaceId,
|
||||
apiKey
|
||||
);
|
||||
|
||||
// Ensure API key is still valid and user has access
|
||||
const { attributes, references } = await services.savedObjectsClient.get<RawAlert>(
|
||||
'alert',
|
||||
alertId
|
||||
);
|
||||
const alert = await alertsClient.get({ id: alertId });
|
||||
|
||||
return {
|
||||
state: await promiseResult<AlertTaskState, Error>(
|
||||
this.validateAndExecuteAlert(services, apiKey, attributes, references)
|
||||
this.validateAndExecuteAlert(services, apiKey, alert)
|
||||
),
|
||||
runAt: asOk(
|
||||
getNextRunAt(
|
||||
|
@ -315,7 +290,7 @@ export class TaskRunner {
|
|||
// we do not currently have a good way of returning the type
|
||||
// from SavedObjectsClient, and as we currenrtly require a schedule
|
||||
// and we only support `interval`, we can cast this safely
|
||||
attributes.schedule as IntervalSchedule
|
||||
alert.schedule
|
||||
)
|
||||
),
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue