* ECS audit events for alerts plugin
* added api changes
* fixed linting and testing errors
* fix test
* Fixed linting errors after prettier update
* Revert "Allow predefined ids for encrypted saved objects (#83482)"
This reverts commit 7d929fe903
.
* Added suggestions from code review
* Fixed unit tests
* Added suggestions from code review
* Changed names of alert events
* Changed naming as suggested in code review
* Added suggestions from PR
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3519a5e1cd
commit
9ef4d22cf3
|
@ -9,7 +9,7 @@ Given a saved object type and id, generates the compound id that is stored in th
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
generateRawId(namespace: string | undefined, type: string, id?: string): string;
|
||||
generateRawId(namespace: string | undefined, type: string, id: string): string;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
static generateId(): string;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`string`
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
static isRandomId(id: string | undefined): boolean;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| id | <code>string | undefined</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`boolean`
|
||||
|
|
@ -19,3 +19,10 @@ export declare class SavedObjectsUtils
|
|||
| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | <code>static</code> | <code>(namespace?: string | undefined) => string</code> | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the <code>undefined</code> namespace ID (which has a namespace string of <code>'default'</code>). |
|
||||
| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | <code>static</code> | <code>(namespace: string) => string | undefined</code> | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the <code>'default'</code> namespace string (which has a namespace ID of <code>undefined</code>). |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [generateId()](./kibana-plugin-core-server.savedobjectsutils.generateid.md) | <code>static</code> | Generates a random ID for a saved objects. |
|
||||
| [isRandomId(id)](./kibana-plugin-core-server.savedobjectsutils.israndomid.md) | <code>static</code> | Validates that a saved object ID matches UUID format. |
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource {
|
|||
*/
|
||||
interface SavedObjectDoc<T = unknown> {
|
||||
attributes: T;
|
||||
id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional
|
||||
id: string;
|
||||
type: string;
|
||||
namespace?: string;
|
||||
namespaces?: string[];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<SavedObject<T>> {
|
||||
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',
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T = unknown> extends Omit<SavedObjec
|
|||
// @public (undocumented)
|
||||
export class SavedObjectsUtils {
|
||||
static createEmptyFindResponse: <T>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T>;
|
||||
static generateId(): string;
|
||||
static isRandomId(id: string | undefined): boolean;
|
||||
static namespaceIdToString: (namespace?: string | undefined) => string;
|
||||
static namespaceStringToId: (namespace: string) => string | undefined;
|
||||
}
|
||||
|
|
|
@ -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<ActionsClient['update']> {
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'my-action-type',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.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<ActionsClient['update']> {
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
minimumLicenseRequired: 'basic',
|
||||
executor,
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'my-action-type',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.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',
|
||||
|
|
|
@ -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<ActionResult> {
|
||||
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<ActionResult> {
|
||||
await this.authorization.ensureAuthorized('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'
|
||||
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<RawAction>(
|
||||
'action',
|
||||
{
|
||||
|
@ -201,12 +252,30 @@ export class ActionsClient {
|
|||
* Get an action
|
||||
*/
|
||||
public async get({ id }: { id: string }): Promise<ActionResult> {
|
||||
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<RawAction>('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<FindActionResult[]> {
|
||||
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<RawAction>({
|
||||
|
@ -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<ActionResult[]> {
|
||||
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<ActionResult>();
|
||||
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<RawAction>(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');
|
||||
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'
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
93
x-pack/plugins/actions/server/lib/audit_events.test.ts
Normal file
93
x-pack/plugins/actions/server/lib/audit_events.test.ts
Normal file
|
@ -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]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
76
x-pack/plugins/actions/server/lib/audit_events.ts
Normal file
76
x-pack/plugins/actions/server/lib/audit_events.ts
Normal file
|
@ -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<ConnectorAuditAction, VerbsTuple> = {
|
||||
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<ConnectorAuditAction, EventType | undefined> = {
|
||||
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<AuditEvent['kibana']>['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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -314,6 +314,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
|
||||
preconfiguredActions,
|
||||
}),
|
||||
auditLogger: this.security?.audit.asScoped(request),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -439,6 +440,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
preconfiguredActions,
|
||||
actionExecutor,
|
||||
instantiateAuthorization,
|
||||
security,
|
||||
} = this;
|
||||
|
||||
return async function actionsRouteHandlerContext(context, request) {
|
||||
|
@ -468,6 +470,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
|
|||
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
|
||||
preconfiguredActions,
|
||||
}),
|
||||
auditLogger: security?.audit.asScoped(request),
|
||||
});
|
||||
},
|
||||
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),
|
||||
|
|
|
@ -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<ActionsClient>;
|
||||
getEventLogClient: () => Promise<IEventLogClient>;
|
||||
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
auditLogger?: AuditLogger;
|
||||
}
|
||||
|
||||
export interface MuteOptions extends IndexType {
|
||||
|
@ -176,6 +180,7 @@ export class AlertsClient {
|
|||
private readonly getEventLogClient: () => Promise<IEventLogClient>;
|
||||
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<Alert> {
|
||||
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<RawAlert>;
|
||||
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<SanitizedAlert> {
|
||||
const result = await this.unsecuredSavedObjectsClient.get<RawAlert>('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<FindResult> {
|
||||
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<RawAlert>('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<RawAlert>(
|
||||
|
|
|
@ -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]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
94
x-pack/plugins/alerts/server/alerts_client/audit_events.ts
Normal file
94
x-pack/plugins/alerts/server/alerts_client/audit_events.ts
Normal file
|
@ -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<AlertAuditAction, VerbsTuple> = {
|
||||
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<AlertAuditAction, EventType> = {
|
||||
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<AuditEvent['kibana']>['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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -40,10 +49,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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',
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -37,10 +40,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -39,10 +41,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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',
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -40,10 +42,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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({
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -45,6 +47,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -41,6 +44,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -42,6 +44,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
|
||||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -44,10 +46,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ConstructorOptions> = {
|
||||
|
@ -38,10 +41,12 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
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',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ export class EncryptedSavedObjectAttributesDefinition {
|
|||
public readonly attributesToEncrypt: ReadonlySet<string>;
|
||||
private readonly attributesToExcludeFromAAD: ReadonlySet<string> | undefined;
|
||||
private readonly attributesToStrip: ReadonlySet<string>;
|
||||
public readonly allowPredefinedID: boolean;
|
||||
|
||||
constructor(typeRegistration: EncryptedSavedObjectTypeRegistration) {
|
||||
const attributesToEncrypt = new Set<string>();
|
||||
|
@ -35,7 +34,6 @@ export class EncryptedSavedObjectAttributesDefinition {
|
|||
this.attributesToEncrypt = attributesToEncrypt;
|
||||
this.attributesToStrip = attributesToStrip;
|
||||
this.attributesToExcludeFromAAD = typeRegistration.attributesToExcludeFromAAD;
|
||||
this.allowPredefinedID = !!typeRegistration.allowPredefinedID;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -31,7 +31,6 @@ export interface EncryptedSavedObjectTypeRegistration {
|
|||
readonly type: string;
|
||||
readonly attributesToEncrypt: ReadonlySet<string | AttributeToEncrypt>;
|
||||
readonly attributesToExcludeFromAAD?: ReadonlySet<string>;
|
||||
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
|
||||
|
|
|
@ -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<SavedObjectsClientContract>;
|
||||
|
@ -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' },
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ Object {
|
|||
"title": "mylens",
|
||||
"visualizationType": "lnsXY",
|
||||
},
|
||||
"id": "mock-saved-object-id",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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<SavedObjectAction, VerbsTuple> = {
|
||||
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<SavedObjectAction, EventType> = {
|
||||
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<Required<AuditEvent>['kibana']>['saved_object'];
|
||||
savedObject?: NonNullable<AuditEvent['kibana']>['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];
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<typeof createSecureSavedObjectsClientWrapperOptions>;
|
||||
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) });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<SavedObjectsBulkCreateObject<T>>,
|
||||
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<T>(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]);
|
||||
}
|
||||
|
|
|
@ -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<PackagePolicy> = {
|
||||
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<PackagePolicy> = {
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue