diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md
index e86d7cbb36435..a9dfd84cf0b42 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.generaterawid.md
@@ -9,7 +9,7 @@ Given a saved object type and id, generates the compound id that is stored in th
Signature:
```typescript
-generateRawId(namespace: string | undefined, type: string, id?: string): string;
+generateRawId(namespace: string | undefined, type: string, id: string): string;
```
## Parameters
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md
new file mode 100644
index 0000000000000..f095184484992
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.generateid.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [generateId](./kibana-plugin-core-server.savedobjectsutils.generateid.md)
+
+## SavedObjectsUtils.generateId() method
+
+Generates a random ID for a saved objects.
+
+Signature:
+
+```typescript
+static generateId(): string;
+```
+Returns:
+
+`string`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md
new file mode 100644
index 0000000000000..7bfb1bcbd8cd7
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.israndomid.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [isRandomId](./kibana-plugin-core-server.savedobjectsutils.israndomid.md)
+
+## SavedObjectsUtils.isRandomId() method
+
+Validates that a saved object ID matches UUID format.
+
+Signature:
+
+```typescript
+static isRandomId(id: string | undefined): boolean;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| id | string | undefined
| |
+
+Returns:
+
+`boolean`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
index 83831f65bd41a..7b774e14b640f 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md
@@ -19,3 +19,10 @@ export declare class SavedObjectsUtils
| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static
| (namespace?: string | undefined) => string
| Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined
namespace ID (which has a namespace string of 'default'
). |
| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static
| (namespace: string) => string | undefined
| Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default'
namespace string (which has a namespace ID of undefined
). |
+## Methods
+
+| Method | Modifiers | Description |
+| --- | --- | --- |
+| [generateId()](./kibana-plugin-core-server.savedobjectsutils.generateid.md) | static
| Generates a random ID for a saved objects. |
+| [isRandomId(id)](./kibana-plugin-core-server.savedobjectsutils.israndomid.md) | static
| Validates that a saved object ID matches UUID format. |
+
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index bacd93f585adc..4b3512ae3056b 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -84,6 +84,14 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is creating a saved object.
| `failure` | User is not authorized to create a saved object.
+.2+| `connector_create`
+| `unknown` | User is creating a connector.
+| `failure` | User is not authorized to create a connector.
+
+.2+| `alert_create`
+| `unknown` | User is creating an alert rule.
+| `failure` | User is not authorized to create an alert rule.
+
3+a|
====== Type: change
@@ -108,6 +116,42 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is removing references to a saved object.
| `failure` | User is not authorized to remove references to a saved object.
+.2+| `connector_update`
+| `unknown` | User is updating a connector.
+| `failure` | User is not authorized to update a connector.
+
+.2+| `alert_update`
+| `unknown` | User is updating an alert rule.
+| `failure` | User is not authorized to update an alert rule.
+
+.2+| `alert_update_api_key`
+| `unknown` | User is updating the API key of an alert rule.
+| `failure` | User is not authorized to update the API key of an alert rule.
+
+.2+| `alert_enable`
+| `unknown` | User is enabling an alert rule.
+| `failure` | User is not authorized to enable an alert rule.
+
+.2+| `alert_disable`
+| `unknown` | User is disabling an alert rule.
+| `failure` | User is not authorized to disable an alert rule.
+
+.2+| `alert_mute`
+| `unknown` | User is muting an alert rule.
+| `failure` | User is not authorized to mute an alert rule.
+
+.2+| `alert_unmute`
+| `unknown` | User is unmuting an alert rule.
+| `failure` | User is not authorized to unmute an alert rule.
+
+.2+| `alert_instance_mute`
+| `unknown` | User is muting an alert instance.
+| `failure` | User is not authorized to mute an alert instance.
+
+.2+| `alert_instance_unmute`
+| `unknown` | User is unmuting an alert instance.
+| `failure` | User is not authorized to unmute an alert instance.
+
3+a|
====== Type: deletion
@@ -120,6 +164,14 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is deleting a saved object.
| `failure` | User is not authorized to delete a saved object.
+.2+| `connector_delete`
+| `unknown` | User is deleting a connector.
+| `failure` | User is not authorized to delete a connector.
+
+.2+| `alert_delete`
+| `unknown` | User is deleting an alert rule.
+| `failure` | User is not authorized to delete an alert rule.
+
3+a|
====== Type: access
@@ -135,6 +187,22 @@ Refer to the corresponding {es} logs for potential write errors.
| `success` | User has accessed a saved object as part of a search operation.
| `failure` | User is not authorized to search for saved objects.
+.2+| `connector_get`
+| `success` | User has accessed a connector.
+| `failure` | User is not authorized to access a connector.
+
+.2+| `connector_find`
+| `success` | User has accessed a connector as part of a search operation.
+| `failure` | User is not authorized to search for connectors.
+
+.2+| `alert_get`
+| `success` | User has accessed an alert rule.
+| `failure` | User is not authorized to access an alert rule.
+
+.2+| `alert_find`
+| `success` | User has accessed an alert rule as part of a search operation.
+| `failure` | User is not authorized to search for alert rules.
+
3+a|
===== Category: web
diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts
index e5f0e8abd3b71..561f9bc001e30 100644
--- a/src/core/server/saved_objects/serialization/serializer.test.ts
+++ b/src/core/server/saved_objects/serialization/serializer.test.ts
@@ -573,24 +573,10 @@ describe('#savedObjectToRaw', () => {
});
describe('single-namespace type without a namespace', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = singleNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- attributes: { bar: true },
- } as any);
-
- const v2 = singleNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`doesn't specify _source.namespace`, () => {
const actual = singleNamespaceSerializer.savedObjectToRaw({
type: '',
+ id: 'mock-saved-object-id',
attributes: {},
} as any);
@@ -599,23 +585,6 @@ describe('#savedObjectToRaw', () => {
});
describe('single-namespace type with a namespace', () => {
- test('generates an id prefixed with namespace and type, if no id is specified', () => {
- const v1 = singleNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- const v2 = singleNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^bar\:foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`it copies namespace to _source.namespace`, () => {
const actual = singleNamespaceSerializer.savedObjectToRaw({
type: 'foo',
@@ -628,23 +597,6 @@ describe('#savedObjectToRaw', () => {
});
describe('single-namespace type with namespaces', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- const v2 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`doesn't specify _source.namespaces`, () => {
const actual = namespaceAgnosticSerializer.savedObjectToRaw({
type: 'foo',
@@ -657,23 +609,6 @@ describe('#savedObjectToRaw', () => {
});
describe('namespace-agnostic type with a namespace', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- const v2 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`doesn't specify _source.namespace`, () => {
const actual = namespaceAgnosticSerializer.savedObjectToRaw({
type: 'foo',
@@ -686,23 +621,6 @@ describe('#savedObjectToRaw', () => {
});
describe('namespace-agnostic type with namespaces', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- const v2 = namespaceAgnosticSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`doesn't specify _source.namespaces`, () => {
const actual = namespaceAgnosticSerializer.savedObjectToRaw({
type: 'foo',
@@ -715,23 +633,6 @@ describe('#savedObjectToRaw', () => {
});
describe('multi-namespace type with a namespace', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = multiNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- const v2 = multiNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespace: 'bar',
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`doesn't specify _source.namespace`, () => {
const actual = multiNamespaceSerializer.savedObjectToRaw({
type: 'foo',
@@ -744,23 +645,6 @@ describe('#savedObjectToRaw', () => {
});
describe('multi-namespace type with namespaces', () => {
- test('generates an id prefixed with type, if no id is specified', () => {
- const v1 = multiNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- const v2 = multiNamespaceSerializer.savedObjectToRaw({
- type: 'foo',
- namespaces: ['bar'],
- attributes: { bar: true },
- } as any);
-
- expect(v1._id).toMatch(/^foo\:[\w-]+$/);
- expect(v1._id).not.toEqual(v2._id);
- });
-
test(`it copies namespaces to _source.namespaces`, () => {
const actual = multiNamespaceSerializer.savedObjectToRaw({
type: 'foo',
@@ -1064,11 +948,6 @@ describe('#isRawSavedObject', () => {
describe('#generateRawId', () => {
describe('single-namespace type without a namespace', () => {
- test('generates an id if none is specified', () => {
- const id = singleNamespaceSerializer.generateRawId('', 'goodbye');
- expect(id).toMatch(/^goodbye\:[\w-]+$/);
- });
-
test('uses the id that is specified', () => {
const id = singleNamespaceSerializer.generateRawId('', 'hello', 'world');
expect(id).toEqual('hello:world');
@@ -1076,11 +955,6 @@ describe('#generateRawId', () => {
});
describe('single-namespace type with a namespace', () => {
- test('generates an id if none is specified and prefixes namespace', () => {
- const id = singleNamespaceSerializer.generateRawId('foo', 'goodbye');
- expect(id).toMatch(/^foo:goodbye\:[\w-]+$/);
- });
-
test('uses the id that is specified and prefixes the namespace', () => {
const id = singleNamespaceSerializer.generateRawId('foo', 'hello', 'world');
expect(id).toEqual('foo:hello:world');
@@ -1088,11 +962,6 @@ describe('#generateRawId', () => {
});
describe('namespace-agnostic type with a namespace', () => {
- test(`generates an id if none is specified and doesn't prefix namespace`, () => {
- const id = namespaceAgnosticSerializer.generateRawId('foo', 'goodbye');
- expect(id).toMatch(/^goodbye\:[\w-]+$/);
- });
-
test(`uses the id that is specified and doesn't prefix the namespace`, () => {
const id = namespaceAgnosticSerializer.generateRawId('foo', 'hello', 'world');
expect(id).toEqual('hello:world');
diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts
index 145dd286c1ca8..82999eeceb887 100644
--- a/src/core/server/saved_objects/serialization/serializer.ts
+++ b/src/core/server/saved_objects/serialization/serializer.ts
@@ -17,7 +17,6 @@
* under the License.
*/
-import uuid from 'uuid';
import { decodeVersion, encodeVersion } from '../version';
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { SavedObjectsRawDoc, SavedObjectSanitizedDoc } from './types';
@@ -127,10 +126,10 @@ export class SavedObjectsSerializer {
* @param {string} type - The saved object type
* @param {string} id - The id of the saved object
*/
- public generateRawId(namespace: string | undefined, type: string, id?: string) {
+ public generateRawId(namespace: string | undefined, type: string, id: string) {
const namespacePrefix =
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
- return `${namespacePrefix}${type}:${id || uuid.v1()}`;
+ return `${namespacePrefix}${type}:${id}`;
}
private trimIdPrefix(namespace: string | undefined, type: string, id: string) {
diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts
index 8b3eebceb2c5a..e59b1a68e1ad1 100644
--- a/src/core/server/saved_objects/serialization/types.ts
+++ b/src/core/server/saved_objects/serialization/types.ts
@@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource {
*/
interface SavedObjectDoc {
attributes: T;
- id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional
+ id: string;
type: string;
namespace?: string;
namespaces?: string[];
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 6a3defb9556f5..a19b4cc01db8e 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -1831,21 +1831,16 @@ describe('SavedObjectsRepository', () => {
};
describe('client calls', () => {
- it(`should use the ES create action if ID is undefined and overwrite=true`, async () => {
+ it(`should use the ES index action if overwrite=true`, async () => {
await createSuccess(type, attributes, { overwrite: true });
- expect(client.create).toHaveBeenCalled();
+ expect(client.index).toHaveBeenCalled();
});
- it(`should use the ES create action if ID is undefined and overwrite=false`, async () => {
+ it(`should use the ES create action if overwrite=false`, async () => {
await createSuccess(type, attributes);
expect(client.create).toHaveBeenCalled();
});
- it(`should use the ES index action if ID is defined and overwrite=true`, async () => {
- await createSuccess(type, attributes, { id, overwrite: true });
- expect(client.index).toHaveBeenCalled();
- });
-
it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => {
await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion });
expect(client.index).toHaveBeenCalled();
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index dae6a8d19dae2..587a0e51ef9b9 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -18,7 +18,6 @@
*/
import { omit, isObject } from 'lodash';
-import uuid from 'uuid';
import {
ElasticsearchClient,
DeleteDocumentResponse,
@@ -245,7 +244,7 @@ export class SavedObjectsRepository {
options: SavedObjectsCreateOptions = {}
): Promise> {
const {
- id,
+ id = SavedObjectsUtils.generateId(),
migrationVersion,
overwrite = false,
references = [],
@@ -366,7 +365,9 @@ export class SavedObjectsRepository {
const method = object.id && overwrite ? 'index' : 'create';
const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
- if (object.id == null) object.id = uuid.v1();
+ if (object.id == null) {
+ object.id = SavedObjectsUtils.generateId();
+ }
return {
tag: 'Right' as 'Right',
diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts
index ac06ca9275783..062a68e2dca28 100644
--- a/src/core/server/saved_objects/service/lib/utils.test.ts
+++ b/src/core/server/saved_objects/service/lib/utils.test.ts
@@ -17,11 +17,22 @@
* under the License.
*/
+import uuid from 'uuid';
import { SavedObjectsFindOptions } from '../../types';
import { SavedObjectsUtils } from './utils';
+jest.mock('uuid', () => ({
+ v1: jest.fn().mockReturnValue('mock-uuid'),
+}));
+
describe('SavedObjectsUtils', () => {
- const { namespaceIdToString, namespaceStringToId, createEmptyFindResponse } = SavedObjectsUtils;
+ const {
+ namespaceIdToString,
+ namespaceStringToId,
+ createEmptyFindResponse,
+ generateId,
+ isRandomId,
+ } = SavedObjectsUtils;
describe('#namespaceIdToString', () => {
it('converts `undefined` to default namespace string', () => {
@@ -77,4 +88,20 @@ describe('SavedObjectsUtils', () => {
expect(createEmptyFindResponse(options).per_page).toEqual(42);
});
});
+
+ describe('#generateId', () => {
+ it('returns a valid uuid', () => {
+ expect(generateId()).toBe('mock-uuid');
+ expect(uuid.v1).toHaveBeenCalled();
+ });
+ });
+
+ describe('#isRandomId', () => {
+ it('validates uuid correctly', () => {
+ expect(isRandomId('c4d82f66-3046-11eb-adc1-0242ac120002')).toBe(true);
+ expect(isRandomId('invalid')).toBe(false);
+ expect(isRandomId('')).toBe(false);
+ expect(isRandomId(undefined)).toBe(false);
+ });
+ });
});
diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts
index 69abc37089218..b59829cb4978a 100644
--- a/src/core/server/saved_objects/service/lib/utils.ts
+++ b/src/core/server/saved_objects/service/lib/utils.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import uuid from 'uuid';
import { SavedObjectsFindOptions } from '../../types';
import { SavedObjectsFindResponse } from '..';
@@ -24,6 +25,7 @@ export const DEFAULT_NAMESPACE_STRING = 'default';
export const ALL_NAMESPACES_STRING = '*';
export const FIND_DEFAULT_PAGE = 1;
export const FIND_DEFAULT_PER_PAGE = 20;
+const UUID_REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
/**
* @public
@@ -69,4 +71,21 @@ export class SavedObjectsUtils {
total: 0,
saved_objects: [],
});
+
+ /**
+ * Generates a random ID for a saved objects.
+ */
+ public static generateId() {
+ return uuid.v1();
+ }
+
+ /**
+ * Validates that a saved object ID has been randomly generated.
+ *
+ * @param {string} id The ID of a saved object.
+ * @todo Use `uuid.validate` once upgraded to v5.3+
+ */
+ public static isRandomId(id: string | undefined) {
+ return typeof id === 'string' && UUID_REGEX.test(id);
+ }
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index d877fc36d114b..770048d2cff13 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2518,7 +2518,7 @@ export interface SavedObjectsResolveImportErrorsOptions {
export class SavedObjectsSerializer {
// @internal
constructor(registry: ISavedObjectTypeRegistry);
- generateRawId(namespace: string | undefined, type: string, id?: string): string;
+ generateRawId(namespace: string | undefined, type: string, id: string): string;
isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean;
rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc;
savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc;
@@ -2600,6 +2600,8 @@ export interface SavedObjectsUpdateResponse extends Omit({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse;
+ static generateId(): string;
+ static isRandomId(id: string | undefined): boolean;
static namespaceIdToString: (namespace?: string | undefined) => string;
static namespaceStringToId: (namespace: string) => string | undefined;
}
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 171f8d4b0b1d4..8b6c25e1c3f24 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -15,6 +15,8 @@ import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '../../licensing/server/mocks';
+import { httpServerMock } from '../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../security/server/audit/index.mock';
import {
elasticsearchServiceMock,
@@ -22,17 +24,23 @@ import {
} from '../../../../src/core/server/mocks';
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';
+jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ({
+ SavedObjectsUtils: {
+ generateId: () => 'mock-saved-object-id',
+ },
+}));
+
const defaultKibanaIndex = '.kibana';
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
const actionExecutor = actionExecutorMock.create();
const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
-const request = {} as KibanaRequest;
+const request = httpServerMock.createKibanaRequest();
+const auditLogger = auditServiceMock.create().asScoped(request);
const mockTaskManager = taskManagerMock.createSetup();
@@ -68,6 +76,7 @@ beforeEach(() => {
executionEnqueuer,
request,
authorization: (authorization as unknown) as ActionsAuthorization,
+ auditLogger,
});
});
@@ -142,6 +151,95 @@ describe('create()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when creating a connector', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: savedObjectCreateResult.attributes.actionTypeId,
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ await actionsClient.create({
+ action: {
+ ...savedObjectCreateResult.attributes,
+ secrets: {},
+ },
+ });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_create',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'mock-saved-object-id', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to create a connector', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: savedObjectCreateResult.attributes.actionTypeId,
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ async () =>
+ await actionsClient.create({
+ action: {
+ ...savedObjectCreateResult.attributes,
+ secrets: {},
+ },
+ })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_create',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: 'mock-saved-object-id',
+ type: 'action',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('creates an action with all given properties', async () => {
const savedObjectCreateResult = {
id: '1',
@@ -185,6 +283,9 @@ describe('create()', () => {
"name": "my name",
"secrets": Object {},
},
+ Object {
+ "id": "mock-saved-object-id",
+ },
]
`);
});
@@ -289,6 +390,9 @@ describe('create()', () => {
"name": "my name",
"secrets": Object {},
},
+ Object {
+ "id": "mock-saved-object-id",
+ },
]
`);
});
@@ -440,7 +544,7 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
});
- test('throws when user is not authorised to create the type of action', async () => {
+ test('throws when user is not authorised to get the type of action', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'type',
@@ -463,7 +567,7 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
});
- test('throws when user is not authorised to create preconfigured of action', async () => {
+ test('throws when user is not authorised to get preconfigured of action', async () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
unsecuredSavedObjectsClient,
@@ -501,6 +605,61 @@ describe('get()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when getting a connector', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ await actionsClient.get({ id: '1' });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to get a connector', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.get({ id: '1' })).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with id', async () => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -632,6 +791,64 @@ describe('getAll()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when searching connectors', async () => {
+ unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+ scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ });
+
+ await actionsClient.getAll();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_find',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search connectors', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.getAll()).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_find',
+ outcome: 'failure',
+ }),
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with parameters', async () => {
const expectedResult = {
total: 1,
@@ -773,6 +990,62 @@ describe('getBulk()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when bulk getting connectors', async () => {
+ 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 },
+ },
+ });
+
+ await actionsClient.getBulk(['1']);
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to bulk get connectors', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.getBulk(['1'])).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_get',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => {
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
@@ -864,6 +1137,39 @@ describe('delete()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when deleting a connector', async () => {
+ await actionsClient.delete({ id: '1' });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_delete',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to delete a connector', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(actionsClient.delete({ id: '1' })).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_delete',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('calls unsecuredSavedObjectsClient with id', async () => {
const expectedResult = Symbol();
unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
@@ -880,42 +1186,43 @@ describe('delete()', () => {
});
describe('update()', () => {
+ function updateOperation(): ReturnType {
+ 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.create.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: {},
+ },
+ });
+ }
+
describe('authorization', () => {
- function updateOperation(): ReturnType {
- 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.create.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');
@@ -934,6 +1241,39 @@ describe('update()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when updating a connector', async () => {
+ await updateOperation();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_update',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'my-action', type: 'action' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to update a connector', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(updateOperation()).rejects.toThrow();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'connector_update',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: 'my-action', type: 'action' } },
+ error: { code: 'Error', message: 'Unauthorized' },
+ })
+ );
+ });
+ });
+
test('updates an action with all given properties', async () => {
actionTypeRegistry.register({
id: 'my-action-type',
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 0d41b520501ad..ab693dc340c92 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from '@hapi/boom';
+
+import { i18n } from '@kbn/i18n';
+import { omitBy, isUndefined } from 'lodash';
import {
ILegacyScopedClusterClient,
SavedObjectsClientContract,
SavedObjectAttributes,
SavedObject,
KibanaRequest,
-} from 'src/core/server';
-
-import { i18n } from '@kbn/i18n';
-import { omitBy, isUndefined } from 'lodash';
+ SavedObjectsUtils,
+} from '../../../../src/core/server';
+import { AuditLogger, EventOutcome } from '../../security/server';
+import { ActionType } from '../common';
import { ActionTypeRegistry } from './action_type_registry';
import { validateConfig, validateSecrets, ActionExecutorContract } from './lib';
import {
@@ -30,11 +33,11 @@ import {
ExecuteOptions as EnqueueExecutionOptions,
} from './create_execute_function';
import { ActionsAuthorization } from './authorization/actions_authorization';
-import { ActionType } from '../common';
import {
getAuthorizationModeBySource,
AuthorizationMode,
} from './authorization/get_authorization_mode_by_source';
+import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events';
// 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.
@@ -65,6 +68,7 @@ interface ConstructorOptions {
executionEnqueuer: ExecutionEnqueuer;
request: KibanaRequest;
authorization: ActionsAuthorization;
+ auditLogger?: AuditLogger;
}
interface UpdateOptions {
@@ -82,6 +86,7 @@ export class ActionsClient {
private readonly request: KibanaRequest;
private readonly authorization: ActionsAuthorization;
private readonly executionEnqueuer: ExecutionEnqueuer;
+ private readonly auditLogger?: AuditLogger;
constructor({
actionTypeRegistry,
@@ -93,6 +98,7 @@ export class ActionsClient {
executionEnqueuer,
request,
authorization,
+ auditLogger,
}: ConstructorOptions) {
this.actionTypeRegistry = actionTypeRegistry;
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
@@ -103,6 +109,7 @@ export class ActionsClient {
this.executionEnqueuer = executionEnqueuer;
this.request = request;
this.authorization = authorization;
+ this.auditLogger = auditLogger;
}
/**
@@ -111,7 +118,20 @@ export class ActionsClient {
public async create({
action: { actionTypeId, name, config, secrets },
}: CreateOptions): Promise {
- await this.authorization.ensureAuthorized('create', actionTypeId);
+ const id = SavedObjectsUtils.generateId();
+
+ try {
+ await this.authorization.ensureAuthorized('create', actionTypeId);
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
const actionType = this.actionTypeRegistry.get(actionTypeId);
const validatedActionTypeConfig = validateConfig(actionType, config);
@@ -119,12 +139,24 @@ export class ActionsClient {
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
- const result = await this.unsecuredSavedObjectsClient.create('action', {
- actionTypeId,
- name,
- config: validatedActionTypeConfig as SavedObjectAttributes,
- secrets: validatedActionTypeSecrets as SavedObjectAttributes,
- });
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id },
+ outcome: EventOutcome.UNKNOWN,
+ })
+ );
+
+ const result = await this.unsecuredSavedObjectsClient.create(
+ 'action',
+ {
+ actionTypeId,
+ name,
+ config: validatedActionTypeConfig as SavedObjectAttributes,
+ secrets: validatedActionTypeSecrets as SavedObjectAttributes,
+ },
+ { id }
+ );
return {
id: result.id,
@@ -139,21 +171,32 @@ export class ActionsClient {
* Update action
*/
public async update({ id, action }: UpdateOptions): Promise {
- await this.authorization.ensureAuthorized('update');
-
- if (
- this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
- undefined
- ) {
- throw new PreconfiguredActionDisabledModificationError(
- i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', {
- defaultMessage: 'Preconfigured action {id} is not allowed to update.',
- values: {
- id,
- },
- }),
- 'update'
+ try {
+ await this.authorization.ensureAuthorized('update');
+
+ if (
+ this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
+ undefined
+ ) {
+ throw new PreconfiguredActionDisabledModificationError(
+ i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', {
+ defaultMessage: 'Preconfigured action {id} is not allowed to update.',
+ values: {
+ id,
+ },
+ }),
+ 'update'
+ );
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.UPDATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
);
+ throw error;
}
const {
attributes,
@@ -168,6 +211,14 @@ export class ActionsClient {
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.UPDATE,
+ savedObject: { type: 'action', id },
+ outcome: EventOutcome.UNKNOWN,
+ })
+ );
+
const result = await this.unsecuredSavedObjectsClient.create(
'action',
{
@@ -201,12 +252,30 @@ export class ActionsClient {
* Get an action
*/
public async get({ id }: { id: string }): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
const preconfiguredActionsList = this.preconfiguredActions.find(
(preconfiguredAction) => preconfiguredAction.id === id
);
if (preconfiguredActionsList !== undefined) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return {
id,
actionTypeId: preconfiguredActionsList.actionTypeId,
@@ -214,8 +283,16 @@ export class ActionsClient {
isPreconfigured: true,
};
}
+
const result = await this.unsecuredSavedObjectsClient.get('action', id);
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return {
id,
actionTypeId: result.attributes.actionTypeId,
@@ -229,7 +306,17 @@ export class ActionsClient {
* Get all actions with preconfigured list
*/
public async getAll(): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.FIND,
+ error,
+ })
+ );
+ throw error;
+ }
const savedObjectsActions = (
await this.unsecuredSavedObjectsClient.find({
@@ -238,6 +325,15 @@ export class ActionsClient {
})
).saved_objects.map(actionFromSavedObject);
+ savedObjectsActions.forEach(({ id }) =>
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.FIND,
+ savedObject: { type: 'action', id },
+ })
+ )
+ );
+
const mergedResult = [
...savedObjectsActions,
...this.preconfiguredActions.map((preconfiguredAction) => ({
@@ -258,7 +354,20 @@ export class ActionsClient {
* Get bulk actions with preconfigured list
*/
public async getBulk(ids: string[]): Promise {
- await this.authorization.ensureAuthorized('get');
+ try {
+ await this.authorization.ensureAuthorized('get');
+ } catch (error) {
+ ids.forEach((id) =>
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ )
+ );
+ throw error;
+ }
const actionResults = new Array();
for (const actionId of ids) {
@@ -283,6 +392,17 @@ export class ActionsClient {
const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' }));
const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet(bulkGetOpts);
+ bulkGetResult.saved_objects.forEach(({ id, error }) => {
+ if (!error && this.auditLogger) {
+ this.auditLogger.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.GET,
+ savedObject: { type: 'action', id },
+ })
+ );
+ }
+ });
+
for (const action of bulkGetResult.saved_objects) {
if (action.error) {
throw Boom.badRequest(
@@ -298,22 +418,42 @@ 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
- ) {
- throw new PreconfiguredActionDisabledModificationError(
- i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', {
- defaultMessage: 'Preconfigured action {id} is not allowed to delete.',
- values: {
- id,
- },
- }),
- 'delete'
+ try {
+ await this.authorization.ensureAuthorized('delete');
+
+ if (
+ this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
+ undefined
+ ) {
+ throw new PreconfiguredActionDisabledModificationError(
+ i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', {
+ defaultMessage: 'Preconfigured action {id} is not allowed to delete.',
+ values: {
+ id,
+ },
+ }),
+ 'delete'
+ );
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.DELETE,
+ savedObject: { type: 'action', id },
+ error,
+ })
);
+ throw error;
}
+
+ this.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.DELETE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'action', id },
+ })
+ );
+
return await this.unsecuredSavedObjectsClient.delete('action', id);
}
diff --git a/x-pack/plugins/actions/server/lib/audit_events.test.ts b/x-pack/plugins/actions/server/lib/audit_events.test.ts
new file mode 100644
index 0000000000000..6c2fd99c2eafd
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/audit_events.test.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { EventOutcome } from '../../../security/server/audit';
+import { ConnectorAuditAction, connectorAuditEvent } from './audit_events';
+
+describe('#connectorAuditEvent', () => {
+ test('creates event with `unknown` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "unknown",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "User is creating connector [id=ACTION_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `success` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "success",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "User has created connector [id=ACTION_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `failure` outcome', () => {
+ expect(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id: 'ACTION_ID' },
+ error: new Error('ERROR_MESSAGE'),
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": Object {
+ "code": "Error",
+ "message": "ERROR_MESSAGE",
+ },
+ "event": Object {
+ "action": "connector_create",
+ "category": "database",
+ "outcome": "failure",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ACTION_ID",
+ "type": "action",
+ },
+ },
+ "message": "Failed attempt to create connector [id=ACTION_ID]",
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/actions/server/lib/audit_events.ts b/x-pack/plugins/actions/server/lib/audit_events.ts
new file mode 100644
index 0000000000000..7d25b5c0cd479
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/audit_events.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { AuditEvent, EventOutcome, EventCategory, EventType } from '../../../security/server';
+
+export enum ConnectorAuditAction {
+ CREATE = 'connector_create',
+ GET = 'connector_get',
+ UPDATE = 'connector_update',
+ DELETE = 'connector_delete',
+ FIND = 'connector_find',
+ EXECUTE = 'connector_execute',
+}
+
+type VerbsTuple = [string, string, string];
+
+const eventVerbs: Record = {
+ connector_create: ['create', 'creating', 'created'],
+ connector_get: ['access', 'accessing', 'accessed'],
+ connector_update: ['update', 'updating', 'updated'],
+ connector_delete: ['delete', 'deleting', 'deleted'],
+ connector_find: ['access', 'accessing', 'accessed'],
+ connector_execute: ['execute', 'executing', 'executed'],
+};
+
+const eventTypes: Record = {
+ connector_create: EventType.CREATION,
+ connector_get: EventType.ACCESS,
+ connector_update: EventType.CHANGE,
+ connector_delete: EventType.DELETION,
+ connector_find: EventType.ACCESS,
+ connector_execute: undefined,
+};
+
+export interface ConnectorAuditEventParams {
+ action: ConnectorAuditAction;
+ outcome?: EventOutcome;
+ savedObject?: NonNullable['saved_object'];
+ error?: Error;
+}
+
+export function connectorAuditEvent({
+ action,
+ savedObject,
+ outcome,
+ error,
+}: ConnectorAuditEventParams): AuditEvent {
+ const doc = savedObject ? `connector [id=${savedObject.id}]` : 'a connector';
+ const [present, progressive, past] = eventVerbs[action];
+ const message = error
+ ? `Failed attempt to ${present} ${doc}`
+ : outcome === EventOutcome.UNKNOWN
+ ? `User is ${progressive} ${doc}`
+ : `User has ${past} ${doc}`;
+ const type = eventTypes[action];
+
+ return {
+ message,
+ event: {
+ action,
+ category: EventCategory.DATABASE,
+ type,
+ outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS),
+ },
+ kibana: {
+ saved_object: savedObject,
+ },
+ error: error && {
+ code: error.name,
+ message: error.message,
+ },
+ };
+}
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index e61936321b8e0..6e37d4bd7a92a 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -314,6 +314,7 @@ export class ActionsPlugin implements Plugin, Plugi
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
}),
+ auditLogger: this.security?.audit.asScoped(request),
});
};
@@ -439,6 +440,7 @@ export class ActionsPlugin implements Plugin, Plugi
preconfiguredActions,
actionExecutor,
instantiateAuthorization,
+ security,
} = this;
return async function actionsRouteHandlerContext(context, request) {
@@ -468,6 +470,7 @@ export class ActionsPlugin implements Plugin, Plugi
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
}),
+ auditLogger: security?.audit.asScoped(request),
});
},
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),
diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
index c83e24c5a45f4..d697817be734b 100644
--- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
@@ -13,7 +13,8 @@ import {
SavedObjectReference,
SavedObject,
PluginInitializerContext,
-} from 'src/core/server';
+ SavedObjectsUtils,
+} from '../../../../../src/core/server';
import { esKuery } from '../../../../../src/plugins/data/server';
import { ActionsClient, ActionsAuthorization } from '../../../actions/server';
import {
@@ -44,10 +45,12 @@ import { IEventLogClient } from '../../../../plugins/event_log/server';
import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date';
import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log';
import { IEvent } from '../../../event_log/server';
+import { AuditLogger, EventOutcome } from '../../../security/server';
import { parseDuration } from '../../common/parse_duration';
import { retryIfConflicts } from '../lib/retry_if_conflicts';
import { partiallyUpdateAlert } from '../saved_objects';
import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation';
+import { alertAuditEvent, AlertAuditAction } from './audit_events';
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
authorizedConsumers: string[];
@@ -75,6 +78,7 @@ export interface ConstructorOptions {
getActionsClient: () => Promise;
getEventLogClient: () => Promise;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
+ auditLogger?: AuditLogger;
}
export interface MuteOptions extends IndexType {
@@ -176,6 +180,7 @@ export class AlertsClient {
private readonly getEventLogClient: () => Promise;
private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
+ private readonly auditLogger?: AuditLogger;
constructor({
alertTypeRegistry,
@@ -192,6 +197,7 @@ export class AlertsClient {
actionsAuthorization,
getEventLogClient,
kibanaVersion,
+ auditLogger,
}: ConstructorOptions) {
this.logger = logger;
this.getUserName = getUserName;
@@ -207,14 +213,28 @@ export class AlertsClient {
this.actionsAuthorization = actionsAuthorization;
this.getEventLogClient = getEventLogClient;
this.kibanaVersion = kibanaVersion;
+ this.auditLogger = auditLogger;
}
public async create({ data, options }: CreateOptions): Promise {
- await this.authorization.ensureAuthorized(
- data.alertTypeId,
- data.consumer,
- WriteOperations.Create
- );
+ const id = SavedObjectsUtils.generateId();
+
+ try {
+ await this.authorization.ensureAuthorized(
+ data.alertTypeId,
+ data.consumer,
+ WriteOperations.Create
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
@@ -248,6 +268,15 @@ export class AlertsClient {
error: null,
},
};
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
let createdAlert: SavedObject;
try {
createdAlert = await this.unsecuredSavedObjectsClient.create(
@@ -256,6 +285,7 @@ export class AlertsClient {
{
...options,
references,
+ id,
}
);
} catch (e) {
@@ -297,10 +327,27 @@ export class AlertsClient {
public async get({ id }: { id: string }): Promise {
const result = await this.unsecuredSavedObjectsClient.get('alert', id);
- await this.authorization.ensureAuthorized(
- result.attributes.alertTypeId,
- result.attributes.consumer,
- ReadOperations.Get
+ try {
+ await this.authorization.ensureAuthorized(
+ result.attributes.alertTypeId,
+ result.attributes.consumer,
+ ReadOperations.Get
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.GET,
+ savedObject: { type: 'alert', id },
+ })
);
return this.getAlertFromRaw(result.id, result.attributes, result.references);
}
@@ -370,11 +417,23 @@ export class AlertsClient {
public async find({
options: { fields, ...options } = {},
}: { options?: FindOptions } = {}): Promise {
+ let authorizationTuple;
+ try {
+ authorizationTuple = await this.authorization.getFindAuthorizationFilter();
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ error,
+ })
+ );
+ throw error;
+ }
const {
filter: authorizationFilter,
ensureAlertTypeIsAuthorized,
logSuccessfulAuthorization,
- } = await this.authorization.getFindAuthorizationFilter();
+ } = authorizationTuple;
const {
page,
@@ -392,7 +451,18 @@ export class AlertsClient {
});
const authorizedData = data.map(({ id, attributes, references }) => {
- ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
+ try {
+ ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
return this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
@@ -400,6 +470,15 @@ export class AlertsClient {
);
});
+ authorizedData.forEach(({ id }) =>
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.FIND,
+ savedObject: { type: 'alert', id },
+ })
+ )
+ );
+
logSuccessfulAuthorization();
return {
@@ -473,10 +552,29 @@ export class AlertsClient {
attributes = alert.attributes;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Delete
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Delete
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DELETE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DELETE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id);
@@ -520,10 +618,30 @@ export class AlertsClient {
// Still attempt to load the object using SOC
alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id);
}
- await this.authorization.ensureAuthorized(
- alertSavedObject.attributes.alertTypeId,
- alertSavedObject.attributes.consumer,
- WriteOperations.Update
+
+ try {
+ await this.authorization.ensureAuthorized(
+ alertSavedObject.attributes.alertTypeId,
+ alertSavedObject.attributes.consumer,
+ WriteOperations.Update
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
@@ -658,14 +776,28 @@ export class AlertsClient {
attributes = alert.attributes;
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UpdateApiKey
- );
- if (attributes.actions.length && !this.authorization.shouldUseLegacyAuthorization(attributes)) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UpdateApiKey
+ );
+ if (
+ attributes.actions.length &&
+ !this.authorization.shouldUseLegacyAuthorization(attributes)
+ ) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE_API_KEY,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
const username = await this.getUserName();
@@ -678,6 +810,15 @@ export class AlertsClient {
updatedAt: new Date().toISOString(),
updatedBy: username,
});
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UPDATE_API_KEY,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
try {
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
} catch (e) {
@@ -732,16 +873,35 @@ export class AlertsClient {
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Enable
- );
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Enable
+ );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.ENABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.ENABLE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
if (attributes.enabled === false) {
const username = await this.getUserName();
const updateAttributes = this.updateMeta({
@@ -816,10 +976,29 @@ export class AlertsClient {
version = alert.version;
}
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.Disable
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Disable
+ );
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DISABLE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
+ }
+
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.DISABLE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
);
if (attributes.enabled === true) {
@@ -866,16 +1045,36 @@ export class AlertsClient {
'alert',
id
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.MuteAll
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
const updateAttributes = this.updateMeta({
muteAll: true,
mutedInstanceIds: [],
@@ -905,16 +1104,36 @@ export class AlertsClient {
'alert',
id
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UnmuteAll
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE,
+ savedObject: { type: 'alert', id },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id },
+ })
+ );
+
const updateAttributes = this.updateMeta({
muteAll: false,
mutedInstanceIds: [],
@@ -945,16 +1164,35 @@ export class AlertsClient {
alertId
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.MuteInstance
- );
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteInstance
+ );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE_INSTANCE,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.MUTE_INSTANCE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
mutedInstanceIds.push(alertInstanceId);
@@ -991,15 +1229,34 @@ export class AlertsClient {
alertId
);
- await this.authorization.ensureAuthorized(
- attributes.alertTypeId,
- attributes.consumer,
- WriteOperations.UnmuteInstance
- );
- if (attributes.actions.length) {
- await this.actionsAuthorization.ensureAuthorized('execute');
+ try {
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteInstance
+ );
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+ } catch (error) {
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE_INSTANCE,
+ savedObject: { type: 'alert', id: alertId },
+ error,
+ })
+ );
+ throw error;
}
+ this.auditLogger?.log(
+ alertAuditEvent({
+ action: AlertAuditAction.UNMUTE_INSTANCE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: alertId },
+ })
+ );
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
await this.unsecuredSavedObjectsClient.update(
diff --git a/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts
new file mode 100644
index 0000000000000..9cd48248320c0
--- /dev/null
+++ b/x-pack/plugins/alerts/server/alerts_client/audit_events.test.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { EventOutcome } from '../../../security/server/audit';
+import { AlertAuditAction, alertAuditEvent } from './audit_events';
+
+describe('#alertAuditEvent', () => {
+ test('creates event with `unknown` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ outcome: EventOutcome.UNKNOWN,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "unknown",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "User is creating alert [id=ALERT_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `success` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": undefined,
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "success",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "User has created alert [id=ALERT_ID]",
+ }
+ `);
+ });
+
+ test('creates event with `failure` outcome', () => {
+ expect(
+ alertAuditEvent({
+ action: AlertAuditAction.CREATE,
+ savedObject: { type: 'alert', id: 'ALERT_ID' },
+ error: new Error('ERROR_MESSAGE'),
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "error": Object {
+ "code": "Error",
+ "message": "ERROR_MESSAGE",
+ },
+ "event": Object {
+ "action": "alert_create",
+ "category": "database",
+ "outcome": "failure",
+ "type": "creation",
+ },
+ "kibana": Object {
+ "saved_object": Object {
+ "id": "ALERT_ID",
+ "type": "alert",
+ },
+ },
+ "message": "Failed attempt to create alert [id=ALERT_ID]",
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/alerts/server/alerts_client/audit_events.ts b/x-pack/plugins/alerts/server/alerts_client/audit_events.ts
new file mode 100644
index 0000000000000..f3e3959824084
--- /dev/null
+++ b/x-pack/plugins/alerts/server/alerts_client/audit_events.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { AuditEvent, EventOutcome, EventCategory, EventType } from '../../../security/server';
+
+export enum AlertAuditAction {
+ CREATE = 'alert_create',
+ GET = 'alert_get',
+ UPDATE = 'alert_update',
+ UPDATE_API_KEY = 'alert_update_api_key',
+ ENABLE = 'alert_enable',
+ DISABLE = 'alert_disable',
+ DELETE = 'alert_delete',
+ FIND = 'alert_find',
+ MUTE = 'alert_mute',
+ UNMUTE = 'alert_unmute',
+ MUTE_INSTANCE = 'alert_instance_mute',
+ UNMUTE_INSTANCE = 'alert_instance_unmute',
+}
+
+type VerbsTuple = [string, string, string];
+
+const eventVerbs: Record = {
+ alert_create: ['create', 'creating', 'created'],
+ alert_get: ['access', 'accessing', 'accessed'],
+ alert_update: ['update', 'updating', 'updated'],
+ alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
+ alert_enable: ['enable', 'enabling', 'enabled'],
+ alert_disable: ['disable', 'disabling', 'disabled'],
+ alert_delete: ['delete', 'deleting', 'deleted'],
+ alert_find: ['access', 'accessing', 'accessed'],
+ alert_mute: ['mute', 'muting', 'muted'],
+ alert_unmute: ['unmute', 'unmuting', 'unmuted'],
+ alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'],
+ alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'],
+};
+
+const eventTypes: Record = {
+ alert_create: EventType.CREATION,
+ alert_get: EventType.ACCESS,
+ alert_update: EventType.CHANGE,
+ alert_update_api_key: EventType.CHANGE,
+ alert_enable: EventType.CHANGE,
+ alert_disable: EventType.CHANGE,
+ alert_delete: EventType.DELETION,
+ alert_find: EventType.ACCESS,
+ alert_mute: EventType.CHANGE,
+ alert_unmute: EventType.CHANGE,
+ alert_instance_mute: EventType.CHANGE,
+ alert_instance_unmute: EventType.CHANGE,
+};
+
+export interface AlertAuditEventParams {
+ action: AlertAuditAction;
+ outcome?: EventOutcome;
+ savedObject?: NonNullable['saved_object'];
+ error?: Error;
+}
+
+export function alertAuditEvent({
+ action,
+ savedObject,
+ outcome,
+ error,
+}: AlertAuditEventParams): AuditEvent {
+ const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert';
+ const [present, progressive, past] = eventVerbs[action];
+ const message = error
+ ? `Failed attempt to ${present} ${doc}`
+ : outcome === EventOutcome.UNKNOWN
+ ? `User is ${progressive} ${doc}`
+ : `User has ${past} ${doc}`;
+ const type = eventTypes[action];
+
+ return {
+ message,
+ event: {
+ action,
+ category: EventCategory.DATABASE,
+ type,
+ outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS),
+ },
+ kibana: {
+ saved_object: savedObject,
+ },
+ error: error && {
+ code: error.name,
+ message: error.message,
+ },
+ };
+}
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
index dcbb33d849405..b943a21ba9bb6 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
@@ -14,15 +14,24 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
+jest.mock('../../../../../../src/core/server/saved_objects/service/lib/utils', () => ({
+ SavedObjectsUtils: {
+ generateId: () => 'mock-saved-object-id',
+ },
+}));
+
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -40,10 +49,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -185,6 +196,62 @@ describe('create()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when creating an alert', async () => {
+ const data = getMockData({
+ enabled: false,
+ actions: [],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: data,
+ references: [],
+ });
+ await alertsClient.create({ data });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_create',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to create an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.create({
+ data: getMockData({
+ enabled: false,
+ actions: [],
+ }),
+ })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_create',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: 'mock-saved-object-id',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('creates an alert', async () => {
const data = getMockData();
const createdAttributes = {
@@ -337,16 +404,17 @@ describe('create()', () => {
}
`);
expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
- Object {
- "references": Array [
- Object {
- "id": "1",
- "name": "action_0",
- "type": "action",
- },
- ],
- }
- `);
+ Object {
+ "id": "mock-saved-object-id",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "action_0",
+ "type": "action",
+ },
+ ],
+ }
+ `);
expect(taskManager.schedule).toHaveBeenCalledTimes(1);
expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -991,6 +1059,7 @@ describe('create()', () => {
},
},
{
+ id: 'mock-saved-object-id',
references: [
{
id: '1',
@@ -1113,6 +1182,7 @@ describe('create()', () => {
},
},
{
+ id: 'mock-saved-object-id',
references: [
{
id: '1',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
index e7b975aec8eb0..a7ef008eaa2ee 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts
@@ -12,6 +12,8 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup } from './lib';
const taskManager = taskManagerMock.createStart();
@@ -20,6 +22,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -37,10 +40,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
describe('delete()', () => {
@@ -239,4 +244,43 @@ describe('delete()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when deleting an alert', async () => {
+ await alertsClient.delete({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_delete',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to delete an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.delete({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_delete',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
index 8c9ab9494a50a..ce0688a5ab2ff 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
@@ -12,16 +12,18 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -39,10 +41,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -109,6 +113,45 @@ describe('disable()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when disabling an alert', async () => {
+ await alertsClient.disable({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_disable',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to disable an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.disable({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_disable',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('disables an alert', async () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
index feec1d1b9334a..daac6689a183b 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
@@ -13,16 +13,18 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
-import { getBeforeSetup, setGlobalDate } from './lib';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { InvalidatePendingApiKey } from '../../types';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -40,10 +42,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -148,6 +152,45 @@ describe('enable()', () => {
});
});
+ describe('auditLogger', () => {
+ test('logs audit event when enabling an alert', async () => {
+ await alertsClient.enable({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_enable',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to enable an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.enable({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_enable',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
+
test('enables an alert', async () => {
const createdAt = new Date().toISOString();
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
index 336cb536d702b..232d48e258256 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
@@ -14,16 +14,18 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -45,6 +47,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -251,4 +254,64 @@ describe('find()', () => {
expect(logSuccessfulAuthorization).toHaveBeenCalled();
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when searching alerts', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ await alertsClient.find();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search alerts', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.find()).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'failure',
+ }),
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to search alert type', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ ensureAlertTypeIsAuthorized: jest.fn(() => {
+ throw new Error('Unauthorized');
+ }),
+ logSuccessfulAuthorization: jest.fn(),
+ });
+
+ await expect(async () => await alertsClient.find()).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_find',
+ outcome: 'failure',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
index 3f0c783f424d1..32ac57459795e 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -191,4 +194,61 @@ describe('get()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get');
});
});
+
+ describe('auditLogger', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: '123',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [],
+ },
+ references: [],
+ });
+ });
+
+ test('logs audit event when getting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ await alertsClient.get({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_get',
+ outcome: 'success',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to get an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.get({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_get',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
index 14ebca2135587..b3c3e1bdd2ede 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
@@ -12,6 +12,8 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@@ -20,6 +22,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -41,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -137,4 +141,85 @@ describe('muteAll()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when muting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ await alertsClient.muteAll({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_mute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to mute an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.muteAll({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_mute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
index c2188f128cb4d..ec69dbdeac55f 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -180,4 +183,75 @@ describe('muteInstance()', () => {
);
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when muting an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_mute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to mute an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_mute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
index d92304ab873be..fd0157091e3a5 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -138,4 +141,85 @@ describe('unmuteAll()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll');
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when unmuting an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ await alertsClient.unmuteAll({ id: '1' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_unmute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to unmute an alert', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ muteAll: false,
+ },
+ references: [],
+ version: '123',
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_unmute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
index 3486df98f2f05..c7d084a01a2a0 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
@@ -12,15 +12,17 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -178,4 +181,75 @@ describe('unmuteInstance()', () => {
);
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when unmuting an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_unmute',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to unmute an alert instance', async () => {
+ const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ schedule: { interval: '10s' },
+ alertTypeId: '2',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_instance_unmute',
+ outcome: 'failure',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
index b42ee096777fe..15fb1e2ec0092 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
@@ -18,15 +18,17 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { resolvable } from '../../test_utils';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
-
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -44,10 +46,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -1302,4 +1306,89 @@ describe('update()', () => {
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update');
});
});
+
+ describe('auditLogger', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ enabled: true,
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [],
+ scheduledTaskId: 'task-123',
+ createdAt: new Date().toISOString(),
+ },
+ updated_at: new Date().toISOString(),
+ references: [],
+ });
+ });
+
+ test('logs audit event when updating an alert', async () => {
+ await alertsClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '10s' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ actions: [],
+ },
+ });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_update',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to update an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(
+ alertsClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '10s' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ actions: [],
+ },
+ })
+ ).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ outcome: 'failure',
+ action: 'alert_update',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
index ca5f44078f513..bf21256bb8413 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
@@ -12,8 +12,10 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup, setGlobalDate } from './lib';
+import { httpServerMock } from '../../../../../../src/core/server/mocks';
+import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { InvalidatePendingApiKey } from '../../types';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -21,6 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertsAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
+const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const kibanaVersion = 'v7.10.0';
const alertsClientParams: jest.Mocked = {
@@ -38,10 +41,12 @@ const alertsClientParams: jest.Mocked = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
+ auditLogger,
};
beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
+ (auditLogger.log as jest.Mock).mockClear();
});
setGlobalDate();
@@ -269,4 +274,44 @@ describe('updateApiKey()', () => {
);
});
});
+
+ describe('auditLogger', () => {
+ test('logs audit event when updating the API key of an alert', async () => {
+ await alertsClient.updateApiKey({ id: '1' });
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ action: 'alert_update_api_key',
+ outcome: 'unknown',
+ }),
+ kibana: { saved_object: { id: '1', type: 'alert' } },
+ })
+ );
+ });
+
+ test('logs audit event when not authorised to update the API key of an alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
+
+ await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow();
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: expect.objectContaining({
+ outcome: 'failure',
+ action: 'alert_update_api_key',
+ }),
+ kibana: {
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ },
+ },
+ error: {
+ code: 'Error',
+ message: 'Unauthorized',
+ },
+ })
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts
index 069703be72f8a..9d71b5f817b2c 100644
--- a/x-pack/plugins/alerts/server/alerts_client_factory.ts
+++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts
@@ -100,6 +100,7 @@ export class AlertsClientFactory {
actionsAuthorization: actions.getActionsAuthorizationWithRequest(request),
namespace: this.spaceIdToNamespace(spaceId),
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
+ auditLogger: securityPluginSetup?.audit.asScoped(request),
async getUserName() {
if (!securityPluginSetup) {
return null;
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts
index f1e06a0cec03d..f528843cf9ea3 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts
@@ -113,18 +113,3 @@ it('correctly determines attribute properties', () => {
}
}
});
-
-it('it correctly sets allowPredefinedID', () => {
- const defaultTypeDefinition = new EncryptedSavedObjectAttributesDefinition({
- type: 'so-type',
- attributesToEncrypt: new Set(['attr#1', 'attr#2']),
- });
- expect(defaultTypeDefinition.allowPredefinedID).toBe(false);
-
- const typeDefinitionWithPredefinedIDAllowed = new EncryptedSavedObjectAttributesDefinition({
- type: 'so-type',
- attributesToEncrypt: new Set(['attr#1', 'attr#2']),
- allowPredefinedID: true,
- });
- expect(typeDefinitionWithPredefinedIDAllowed.allowPredefinedID).toBe(true);
-});
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts
index 398a64585411a..849a2888b6e1a 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts
@@ -15,7 +15,6 @@ export class EncryptedSavedObjectAttributesDefinition {
public readonly attributesToEncrypt: ReadonlySet;
private readonly attributesToExcludeFromAAD: ReadonlySet | undefined;
private readonly attributesToStrip: ReadonlySet;
- public readonly allowPredefinedID: boolean;
constructor(typeRegistration: EncryptedSavedObjectTypeRegistration) {
const attributesToEncrypt = new Set();
@@ -35,7 +34,6 @@ export class EncryptedSavedObjectAttributesDefinition {
this.attributesToEncrypt = attributesToEncrypt;
this.attributesToStrip = attributesToStrip;
this.attributesToExcludeFromAAD = typeRegistration.attributesToExcludeFromAAD;
- this.allowPredefinedID = !!typeRegistration.allowPredefinedID;
}
/**
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts
index 0138e929ca1ca..c692d8698771f 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts
@@ -13,7 +13,6 @@ import {
function createEncryptedSavedObjectsServiceMock() {
return ({
isRegistered: jest.fn(),
- canSpecifyID: jest.fn(),
stripOrDecryptAttributes: jest.fn(),
encryptAttributes: jest.fn(),
decryptAttributes: jest.fn(),
@@ -53,12 +52,6 @@ export const encryptedSavedObjectsServiceMock = {
mock.isRegistered.mockImplementation(
(type) => registrations.findIndex((r) => r.type === type) >= 0
);
- mock.canSpecifyID.mockImplementation((type, version, overwrite) => {
- const registration = registrations.find((r) => r.type === type);
- return (
- registration === undefined || registration.allowPredefinedID || !!(version && overwrite)
- );
- });
mock.encryptAttributes.mockImplementation(async (descriptor, attrs) =>
processAttributes(
descriptor,
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
index 6bc4a392064e4..88d57072697fe 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
@@ -89,45 +89,6 @@ describe('#isRegistered', () => {
});
});
-describe('#canSpecifyID', () => {
- it('returns true for unknown types', () => {
- expect(service.canSpecifyID('unknown-type')).toBe(true);
- });
-
- it('returns true for types registered setting allowPredefinedID to true', () => {
- service.registerType({
- type: 'known-type-1',
- attributesToEncrypt: new Set(['attr-1']),
- allowPredefinedID: true,
- });
- expect(service.canSpecifyID('known-type-1')).toBe(true);
- });
-
- it('returns true when overwriting a saved object with a version specified even when allowPredefinedID is not set', () => {
- service.registerType({
- type: 'known-type-1',
- attributesToEncrypt: new Set(['attr-1']),
- });
- expect(service.canSpecifyID('known-type-1', '2', true)).toBe(true);
- expect(service.canSpecifyID('known-type-1', '2', false)).toBe(false);
- expect(service.canSpecifyID('known-type-1', undefined, true)).toBe(false);
- });
-
- it('returns false for types registered without setting allowPredefinedID', () => {
- service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attr-1']) });
- expect(service.canSpecifyID('known-type-1')).toBe(false);
- });
-
- it('returns false for types registered setting allowPredefinedID to false', () => {
- service.registerType({
- type: 'known-type-1',
- attributesToEncrypt: new Set(['attr-1']),
- allowPredefinedID: false,
- });
- expect(service.canSpecifyID('known-type-1')).toBe(false);
- });
-});
-
describe('#stripOrDecryptAttributes', () => {
it('does not strip attributes from unknown types', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts
index 8d2ebb575c35e..1f1093a179538 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts
@@ -31,7 +31,6 @@ export interface EncryptedSavedObjectTypeRegistration {
readonly type: string;
readonly attributesToEncrypt: ReadonlySet;
readonly attributesToExcludeFromAAD?: ReadonlySet;
- readonly allowPredefinedID?: boolean;
}
/**
@@ -145,25 +144,6 @@ export class EncryptedSavedObjectsService {
return this.typeDefinitions.has(type);
}
- /**
- * Checks whether ID can be specified for the provided saved object.
- *
- * If the type isn't registered as an encrypted saved object, or when overwriting an existing
- * saved object with a version specified, this will return "true".
- *
- * @param type Saved object type.
- * @param version Saved object version number which changes on each successful write operation.
- * Can be used in conjunction with `overwrite` for implementing optimistic concurrency
- * control.
- * @param overwrite Overwrite existing documents.
- */
- public canSpecifyID(type: string, version?: string, overwrite?: boolean) {
- const typeDefinition = this.typeDefinitions.get(type);
- return (
- typeDefinition === undefined || typeDefinition.allowPredefinedID || !!(version && overwrite)
- );
- }
-
/**
* Takes saved object attributes for the specified type and, depending on the type definition,
* either decrypts or strips encrypted attributes (e.g. in case AAD or encryption key has changed
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
index 3c722ccfabae2..85ec08fb7388d 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
@@ -13,7 +13,18 @@ import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/s
import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock';
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
-jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') }));
+jest.mock('../../../../../src/core/server/saved_objects/service/lib/utils', () => {
+ const { SavedObjectsUtils } = jest.requireActual(
+ '../../../../../src/core/server/saved_objects/service/lib/utils'
+ );
+ return {
+ SavedObjectsUtils: {
+ namespaceStringToId: SavedObjectsUtils.namespaceStringToId,
+ isRandomId: SavedObjectsUtils.isRandomId,
+ generateId: () => 'mock-saved-object-id',
+ },
+ };
+});
let wrapper: EncryptedSavedObjectsClientWrapper;
let mockBaseClient: jest.Mocked;
@@ -30,11 +41,6 @@ beforeEach(() => {
{ key: 'attrNotSoSecret', dangerouslyExposeValue: true },
]),
},
- {
- type: 'known-type-predefined-id',
- attributesToEncrypt: new Set(['attrSecret']),
- allowPredefinedID: true,
- },
]);
wrapper = new EncryptedSavedObjectsClientWrapper({
@@ -77,36 +83,16 @@ describe('#create', () => {
expect(mockBaseClient.create).toHaveBeenCalledWith('unknown-type', attributes, options);
});
- it('fails if type is registered without allowPredefinedID and ID is specified', async () => {
+ it('fails if type is registered and ID is specified', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
await expect(wrapper.create('known-type', attributes, { id: 'some-id' })).rejects.toThrowError(
- 'Predefined IDs are not allowed for encrypted saved objects of type "known-type".'
+ 'Predefined IDs are not allowed for saved objects with encrypted attributes, unless the ID has been generated using `SavedObjectsUtils.generateId`.'
);
expect(mockBaseClient.create).not.toHaveBeenCalled();
});
- it('succeeds if type is registered with allowPredefinedID and ID is specified', async () => {
- const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
- const mockedResponse = {
- id: 'some-id',
- type: 'known-type-predefined-id',
- attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
- references: [],
- };
-
- mockBaseClient.create.mockResolvedValue(mockedResponse);
- await expect(
- wrapper.create('known-type-predefined-id', attributes, { id: 'some-id' })
- ).resolves.toEqual({
- ...mockedResponse,
- attributes: { attrOne: 'one', attrThree: 'three' },
- });
-
- expect(mockBaseClient.create).toHaveBeenCalled();
- });
-
it('allows a specified ID when overwriting an existing object', async () => {
const attributes = {
attrOne: 'one',
@@ -168,7 +154,7 @@ describe('#create', () => {
};
const options = { overwrite: true };
const mockedResponse = {
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
type: 'known-type',
attributes: {
attrOne: 'one',
@@ -188,7 +174,7 @@ describe('#create', () => {
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
- { type: 'known-type', id: 'uuid-v4-id' },
+ { type: 'known-type', id: 'mock-saved-object-id' },
{
attrOne: 'one',
attrSecret: 'secret',
@@ -207,7 +193,7 @@ describe('#create', () => {
attrNotSoSecret: '*not-so-secret*',
attrThree: 'three',
},
- { id: 'uuid-v4-id', overwrite: true }
+ { id: 'mock-saved-object-id', overwrite: true }
);
});
@@ -216,7 +202,7 @@ describe('#create', () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { overwrite: true, namespace };
const mockedResponse = {
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
type: 'known-type',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
references: [],
@@ -233,7 +219,7 @@ describe('#create', () => {
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
@@ -244,7 +230,7 @@ describe('#create', () => {
expect(mockBaseClient.create).toHaveBeenCalledWith(
'known-type',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
- { id: 'uuid-v4-id', overwrite: true, namespace }
+ { id: 'mock-saved-object-id', overwrite: true, namespace }
);
};
@@ -270,7 +256,7 @@ describe('#create', () => {
expect(mockBaseClient.create).toHaveBeenCalledWith(
'known-type',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
- { id: 'uuid-v4-id' }
+ { id: 'mock-saved-object-id' }
);
});
});
@@ -282,7 +268,7 @@ describe('#bulkCreate', () => {
const mockedResponse = {
saved_objects: [
{
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
type: 'known-type',
attributes,
references: [],
@@ -315,7 +301,7 @@ describe('#bulkCreate', () => {
[
{
...bulkCreateParams[0],
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
},
bulkCreateParams[1],
@@ -324,7 +310,7 @@ describe('#bulkCreate', () => {
);
});
- it('fails if ID is specified for registered type without allowPredefinedID', async () => {
+ it('fails if ID is specified for registered type', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const bulkCreateParams = [
@@ -333,48 +319,12 @@ describe('#bulkCreate', () => {
];
await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError(
- 'Predefined IDs are not allowed for encrypted saved objects of type "known-type".'
+ 'Predefined IDs are not allowed for saved objects with encrypted attributes, unless the ID has been generated using `SavedObjectsUtils.generateId`.'
);
expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled();
});
- it('succeeds if ID is specified for registered type with allowPredefinedID', async () => {
- const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
- const options = { namespace: 'some-namespace' };
- const mockedResponse = {
- saved_objects: [
- {
- id: 'some-id',
- type: 'known-type-predefined-id',
- attributes,
- references: [],
- },
- {
- id: 'some-id',
- type: 'unknown-type',
- attributes,
- references: [],
- },
- ],
- };
- mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse);
-
- const bulkCreateParams = [
- { id: 'some-id', type: 'known-type-predefined-id', attributes },
- { type: 'unknown-type', attributes },
- ];
-
- await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({
- saved_objects: [
- { ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } },
- mockedResponse.saved_objects[1],
- ],
- });
-
- expect(mockBaseClient.bulkCreate).toHaveBeenCalled();
- });
-
it('allows a specified ID when overwriting an existing object', async () => {
const attributes = {
attrOne: 'one',
@@ -456,7 +406,7 @@ describe('#bulkCreate', () => {
const mockedResponse = {
saved_objects: [
{
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
type: 'known-type',
attributes: { ...attributes, attrSecret: '*secret*', attrNotSoSecret: '*not-so-secret*' },
references: [],
@@ -489,7 +439,7 @@ describe('#bulkCreate', () => {
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
- { type: 'known-type', id: 'uuid-v4-id' },
+ { type: 'known-type', id: 'mock-saved-object-id' },
{
attrOne: 'one',
attrSecret: 'secret',
@@ -504,7 +454,7 @@ describe('#bulkCreate', () => {
[
{
...bulkCreateParams[0],
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
attributes: {
attrOne: 'one',
attrSecret: '*secret*',
@@ -523,7 +473,9 @@ describe('#bulkCreate', () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { namespace };
const mockedResponse = {
- saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }],
+ saved_objects: [
+ { id: 'mock-saved-object-id', type: 'known-type', attributes, references: [] },
+ ],
};
mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse);
@@ -542,7 +494,7 @@ describe('#bulkCreate', () => {
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
@@ -554,7 +506,7 @@ describe('#bulkCreate', () => {
[
{
...bulkCreateParams[0],
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
},
],
@@ -590,7 +542,7 @@ describe('#bulkCreate', () => {
[
{
type: 'known-type',
- id: 'uuid-v4-id',
+ id: 'mock-saved-object-id',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
},
],
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
index ddef9f477433c..313e7c7da9eba 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import uuid from 'uuid';
import {
SavedObject,
SavedObjectsBaseOptions,
@@ -25,7 +24,8 @@ import {
SavedObjectsRemoveReferencesToOptions,
ISavedObjectTypeRegistry,
SavedObjectsRemoveReferencesToResponse,
-} from 'src/core/server';
+ SavedObjectsUtils,
+} from '../../../../../src/core/server';
import { AuthenticatedUser } from '../../../security/common/model';
import { EncryptedSavedObjectsService } from '../crypto';
import { getDescriptorNamespace } from './get_descriptor_namespace';
@@ -37,14 +37,6 @@ interface EncryptedSavedObjectsClientOptions {
getCurrentUser: () => AuthenticatedUser | undefined;
}
-/**
- * Generates UUIDv4 ID for the any newly created saved object that is supposed to contain
- * encrypted attributes.
- */
-function generateID() {
- return uuid.v4();
-}
-
export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientContract {
constructor(
private readonly options: EncryptedSavedObjectsClientOptions,
@@ -67,19 +59,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
return await this.options.baseClient.create(type, attributes, options);
}
- // Saved objects with encrypted attributes should have IDs that are hard to guess especially
- // since IDs are part of the AAD used during encryption. Types can opt-out of this restriction,
- // when necessary, but it's much safer for this wrapper to generate them.
- if (
- options.id &&
- !this.options.service.canSpecifyID(type, options.version, options.overwrite)
- ) {
- throw new Error(
- `Predefined IDs are not allowed for encrypted saved objects of type "${type}".`
- );
- }
-
- const id = options.id ?? generateID();
+ const id = getValidId(options.id, options.version, options.overwrite);
const namespace = getDescriptorNamespace(
this.options.baseTypeRegistry,
type,
@@ -113,19 +93,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
return object;
}
- // Saved objects with encrypted attributes should have IDs that are hard to guess especially
- // since IDs are part of the AAD used during encryption, that's why we control them within this
- // wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document.
- if (
- object.id &&
- !this.options.service.canSpecifyID(object.type, object.version, options?.overwrite)
- ) {
- throw new Error(
- `Predefined IDs are not allowed for encrypted saved objects of type "${object.type}".`
- );
- }
-
- const id = object.id ?? generateID();
+ const id = getValidId(object.id, object.version, options?.overwrite);
const namespace = getDescriptorNamespace(
this.options.baseTypeRegistry,
object.type,
@@ -327,3 +295,26 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
return response;
}
}
+
+// Saved objects with encrypted attributes should have IDs that are hard to guess especially
+// since IDs are part of the AAD used during encryption, that's why we control them within this
+// wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document.
+function getValidId(
+ id: string | undefined,
+ version: string | undefined,
+ overwrite: boolean | undefined
+) {
+ if (id) {
+ // only allow a specified ID if we're overwriting an existing ESO with a Version
+ // this helps us ensure that the document really was previously created using ESO
+ // and not being used to get around the specified ID limitation
+ const canSpecifyID = (overwrite && version) || SavedObjectsUtils.isRandomId(id);
+ if (!canSpecifyID) {
+ throw new Error(
+ 'Predefined IDs are not allowed for saved objects with encrypted attributes, unless the ID has been generated using `SavedObjectsUtils.generateId`.'
+ );
+ }
+ return id;
+ }
+ return SavedObjectsUtils.generateId();
+}
diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
index 4979438dbd3d0..819fbd9c970ce 100644
--- a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
+++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
@@ -156,6 +156,7 @@ Object {
"title": "mylens",
"visualizationType": "lnsXY",
},
+ "id": "mock-saved-object-id",
"references": Array [
Object {
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts
index 957da5cbb3743..9764926fc03fc 100644
--- a/x-pack/plugins/lens/server/migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations.test.ts
@@ -13,6 +13,7 @@ describe('Lens migrations', () => {
const example = {
type: 'lens',
+ id: 'mock-saved-object-id',
attributes: {
expression:
'kibana\n| kibana_context query="{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"}" \n| lens_merge_tables layerIds="c61a8afb-a185-4fae-a064-fb3846f6c451" \n tables={esaggs index="logstash-*" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\",\\"enabled\\":true,\\"type\\":\\"max\\",\\"schema\\":\\"metric\\",\\"params\\":{\\"field\\":\\"bytes\\"}}]" | lens_rename_columns idMap="{\\"col-0-2cd09808-3915-49f4-b3b0-82767eba23f7\\":\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\"}"}\n| lens_metric_chart title="Maximum of bytes" accessor="2cd09808-3915-49f4-b3b0-82767eba23f7"',
@@ -164,6 +165,7 @@ describe('Lens migrations', () => {
const example = {
type: 'lens',
+ id: 'mock-saved-object-id',
attributes: {
expression: `kibana
| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]"
@@ -265,6 +267,7 @@ describe('Lens migrations', () => {
it('should handle pre-migrated expression', () => {
const input = {
type: 'lens',
+ id: 'mock-saved-object-id',
attributes: {
...example.attributes,
expression: `kibana
@@ -283,6 +286,7 @@ describe('Lens migrations', () => {
const context = {} as SavedObjectMigrationContext;
const example = {
+ id: 'mock-saved-object-id',
attributes: {
description: '',
expression:
@@ -513,6 +517,7 @@ describe('Lens migrations', () => {
const example = {
type: 'lens',
+ id: 'mock-saved-object-id',
attributes: {
state: {
datasourceStates: {
diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts
index 6aba78c936071..2e003b1d55eac 100644
--- a/x-pack/plugins/security/server/audit/audit_events.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.ts
@@ -45,7 +45,7 @@ export interface AuditEvent {
*/
saved_object?: {
type: string;
- id?: string;
+ id: string;
};
/**
* Any additional event specific fields.
@@ -178,7 +178,9 @@ export enum SavedObjectAction {
REMOVE_REFERENCES = 'saved_object_remove_references',
}
-const eventVerbs = {
+type VerbsTuple = [string, string, string];
+
+const eventVerbs: Record = {
saved_object_create: ['create', 'creating', 'created'],
saved_object_get: ['access', 'accessing', 'accessed'],
saved_object_update: ['update', 'updating', 'updated'],
@@ -193,7 +195,7 @@ const eventVerbs = {
],
};
-const eventTypes = {
+const eventTypes: Record = {
saved_object_create: EventType.CREATION,
saved_object_get: EventType.ACCESS,
saved_object_update: EventType.CHANGE,
@@ -204,10 +206,10 @@ const eventTypes = {
saved_object_remove_references: EventType.CHANGE,
};
-export interface SavedObjectParams {
+export interface SavedObjectEventParams {
action: SavedObjectAction;
outcome?: EventOutcome;
- savedObject?: Required['kibana']>['saved_object'];
+ savedObject?: NonNullable['saved_object'];
addToSpaces?: readonly string[];
deleteFromSpaces?: readonly string[];
error?: Error;
@@ -220,12 +222,12 @@ export function savedObjectEvent({
deleteFromSpaces,
outcome,
error,
-}: SavedObjectParams): AuditEvent | undefined {
+}: SavedObjectEventParams): AuditEvent | undefined {
const doc = savedObject ? `${savedObject.type} [id=${savedObject.id}]` : 'saved objects';
const [present, progressive, past] = eventVerbs[action];
const message = error
? `Failed attempt to ${present} ${doc}`
- : outcome === 'unknown'
+ : outcome === EventOutcome.UNKNOWN
? `User is ${progressive} ${doc}`
: `User has ${past} ${doc}`;
const type = eventTypes[action];
diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts
index 04db65f88cda0..d99fbc702a078 100644
--- a/x-pack/plugins/security/server/index.ts
+++ b/x-pack/plugins/security/server/index.ts
@@ -27,7 +27,14 @@ export {
SAMLLogin,
OIDCLogin,
} from './authentication';
-export { LegacyAuditLogger } from './audit';
+export {
+ LegacyAuditLogger,
+ AuditLogger,
+ AuditEvent,
+ EventCategory,
+ EventType,
+ EventOutcome,
+} from './audit';
export { SecurityPluginSetup };
export { AuthenticatedUser } from '../common/model';
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
index c6f4ca6dd8afe..15ca8bac89bd6 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
@@ -12,6 +12,18 @@ import { SavedObjectsClientContract } from 'kibana/server';
import { SavedObjectActions } from '../authorization/actions/saved_object';
import { AuditEvent, EventOutcome } from '../audit';
+jest.mock('../../../../../src/core/server/saved_objects/service/lib/utils', () => {
+ const { SavedObjectsUtils } = jest.requireActual(
+ '../../../../../src/core/server/saved_objects/service/lib/utils'
+ );
+ return {
+ SavedObjectsUtils: {
+ createEmptyFindResponse: SavedObjectsUtils.createEmptyFindResponse,
+ generateId: () => 'mock-saved-object-id',
+ },
+ };
+});
+
let clientOpts: ReturnType;
let client: SecureSavedObjectsClientWrapper;
const USERNAME = Symbol();
@@ -551,7 +563,7 @@ describe('#bulkGet', () => {
});
test(`adds audit event when successful`, async () => {
- const apiCallReturnValue = { saved_objects: [], foo: 'bar' };
+ const apiCallReturnValue = { saved_objects: [obj1, obj2], foo: 'bar' };
clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any);
const objects = [obj1, obj2];
const options = { namespace };
@@ -686,7 +698,7 @@ describe('#create', () => {
});
test(`throws decorated ForbiddenError when unauthorized`, async () => {
- const options = { namespace };
+ const options = { id: 'mock-saved-object-id', namespace };
await expectForbiddenError(client.create, { type, attributes, options });
});
@@ -694,8 +706,12 @@ describe('#create', () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any);
- const options = { namespace };
- const result = await expectSuccess(client.create, { type, attributes, options });
+ const options = { id: 'mock-saved-object-id', namespace };
+ const result = await expectSuccess(client.create, {
+ type,
+ attributes,
+ options,
+ });
expect(result).toBe(apiCallReturnValue);
});
@@ -721,17 +737,17 @@ describe('#create', () => {
test(`adds audit event when successful`, async () => {
const apiCallReturnValue = Symbol();
clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any);
- const options = { namespace };
+ const options = { id: 'mock-saved-object-id', namespace };
await expectSuccess(client.create, { type, attributes, options });
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
- expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type });
+ expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type, id: expect.any(String) });
});
test(`adds audit event when not successful`, async () => {
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
await expect(() => client.create(type, attributes, { namespace })).rejects.toThrow();
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
- expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type });
+ expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type, id: expect.any(String) });
});
});
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
index e6e34de4ac9ab..765274a839efa 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
@@ -96,15 +96,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
attributes: T = {} as T,
options: SavedObjectsCreateOptions = {}
) {
- const namespaces = [options.namespace, ...(options.initialNamespaces || [])];
+ const optionsWithId = { ...options, id: options.id ?? SavedObjectsUtils.generateId() };
+ const namespaces = [optionsWithId.namespace, ...(optionsWithId.initialNamespaces || [])];
try {
- const args = { type, attributes, options };
+ const args = { type, attributes, options: optionsWithId };
await this.ensureAuthorized(type, 'create', namespaces, { args });
} catch (error) {
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.CREATE,
- savedObject: { type, id: options.id },
+ savedObject: { type, id: optionsWithId.id },
error,
})
);
@@ -114,11 +115,11 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
savedObjectEvent({
action: SavedObjectAction.CREATE,
outcome: EventOutcome.UNKNOWN,
- savedObject: { type, id: options.id },
+ savedObject: { type, id: optionsWithId.id },
})
);
- const savedObject = await this.baseClient.create(type, attributes, options);
+ const savedObject = await this.baseClient.create(type, attributes, optionsWithId);
return await this.redactSavedObjectNamespaces(savedObject, namespaces);
}
@@ -141,17 +142,26 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
objects: Array>,
options: SavedObjectsBaseOptions = {}
) {
- const namespaces = objects.reduce(
+ const objectsWithId = objects.map((obj) => ({
+ ...obj,
+ id: obj.id ?? SavedObjectsUtils.generateId(),
+ }));
+ const namespaces = objectsWithId.reduce(
(acc, { initialNamespaces = [] }) => acc.concat(initialNamespaces),
[options.namespace]
);
try {
- const args = { objects, options };
- await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, {
- args,
- });
+ const args = { objects: objectsWithId, options };
+ await this.ensureAuthorized(
+ this.getUniqueObjectTypes(objectsWithId),
+ 'bulk_create',
+ namespaces,
+ {
+ args,
+ }
+ );
} catch (error) {
- objects.forEach(({ type, id }) =>
+ objectsWithId.forEach(({ type, id }) =>
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.CREATE,
@@ -162,7 +172,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
);
throw error;
}
- objects.forEach(({ type, id }) =>
+ objectsWithId.forEach(({ type, id }) =>
this.auditLogger.log(
savedObjectEvent({
action: SavedObjectAction.CREATE,
@@ -172,7 +182,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
)
);
- const response = await this.baseClient.bulkCreate(objects, options);
+ const response = await this.baseClient.bulkCreate(objectsWithId, options);
return await this.redactSavedObjectsNamespaces(response, namespaces);
}
@@ -284,14 +294,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
const response = await this.baseClient.bulkGet(objects, options);
- objects.forEach(({ type, id }) =>
- this.auditLogger.log(
- savedObjectEvent({
- action: SavedObjectAction.GET,
- savedObject: { type, id },
- })
- )
- );
+ response.saved_objects.forEach(({ error, type, id }) => {
+ if (!error) {
+ this.auditLogger.log(
+ savedObjectEvent({
+ action: SavedObjectAction.GET,
+ savedObject: { type, id },
+ })
+ );
+ }
+ });
return await this.redactSavedObjectsNamespaces(response, [options.namespace]);
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
index b516f7c57a96d..1b70a13935b7d 100644
--- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
@@ -12,6 +12,7 @@ describe('7.11.0 Endpoint Package Policy migration', () => {
const migration = migratePackagePolicyToV7110;
it('adds malware notification checkbox and optional message and adds AV registration config', () => {
const doc: SavedObjectUnsanitizedDoc = {
+ id: 'mock-saved-object-id',
attributes: {
name: 'Some Policy Name',
package: {
@@ -100,11 +101,13 @@ describe('7.11.0 Endpoint Package Policy migration', () => {
],
},
type: ' nested',
+ id: 'mock-saved-object-id',
});
});
it('does not modify non-endpoint package policies', () => {
const doc: SavedObjectUnsanitizedDoc = {
+ id: 'mock-saved-object-id',
attributes: {
name: 'Some Policy Name',
package: {
@@ -164,6 +167,7 @@ describe('7.11.0 Endpoint Package Policy migration', () => {
],
},
type: ' nested',
+ id: 'mock-saved-object-id',
});
});
});