From 13c41c07812d6070e0153c970a90511d84d98fce Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 7 Jul 2020 22:16:39 +0300 Subject: [PATCH] [Audit Logging] Add AuditTrail service (#69278) * add generic audit_trail service in core * expose auditTraik service to plugins * add auditTrail x-pack plugin * fix type errors * update mocks * expose asScoped interface via start. auditor via request context * use type from audit trail service * wrap getActiveSpace in safeCall only. it throws exception for non-authz * pass message to log explicitly * update docs * create one auditor per request * wire es client up to auditor * update docs * withScope accepts only one scope * use scoped client in context for callAsInternalUser * use auditor in scoped cluster client * adopt auditTrail plugin to new interface. configure log from config * do not log audit events in console by default * add audit trail functional tests * cleanup * add example * add mocks for spaces plugin * add unit tests * update docs * test description * Apply suggestions from code review apply @jportner suggestions Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * add unit tests * more robust tests * make spaces optional * address comments * update docs * fix WebStorm refactoring Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- ...ibana-plugin-core-server.auditableevent.md | 25 +++ ...ugin-core-server.auditableevent.message.md | 11 ++ ...-plugin-core-server.auditableevent.type.md | 11 ++ .../kibana-plugin-core-server.auditor.add.md | 36 +++++ .../kibana-plugin-core-server.auditor.md | 21 +++ ...ugin-core-server.auditor.withauditscope.md | 24 +++ ...gin-core-server.auditorfactory.asscoped.md | 22 +++ ...ibana-plugin-core-server.auditorfactory.md | 20 +++ ...bana-plugin-core-server.audittrailsetup.md | 18 +++ ...in-core-server.audittrailsetup.register.md | 24 +++ ...bana-plugin-core-server.audittrailstart.md | 11 ++ ...plugin-core-server.coresetup.audittrail.md | 13 ++ .../kibana-plugin-core-server.coresetup.md | 1 + ...plugin-core-server.corestart.audittrail.md | 13 ++ .../kibana-plugin-core-server.corestart.md | 1 + ...gin-core-server.httpserverinfo.hostname.md | 13 ++ ...erver.legacyclusterclient._constructor_.md | 3 +- ...-plugin-core-server.legacyclusterclient.md | 2 +- ...legacyscopedclusterclient._constructor_.md | 3 +- ...n-core-server.legacyscopedclusterclient.md | 2 +- .../core/server/kibana-plugin-core-server.md | 5 + ...-core-server.requesthandlercontext.core.md | 1 + ...lugin-core-server.requesthandlercontext.md | 2 +- ...tils-common-state_containers.comparator.md | 2 +- ...a_utils-common-state_containers.connect.md | 2 +- ...state_containers.createstatecontainer_1.md | 2 +- ...state_containers.createstatecontainer_2.md | 2 +- ...ners.createstatecontaineroptions.freeze.md | 2 +- ..._containers.createstatecontaineroptions.md | 2 +- ...ns-kibana_utils-common-state_containers.md | 12 +- ...tate_containers.reduxlikestatecontainer.md | 2 +- ...-common-state_containers.statecontainer.md | 2 +- .../kibana_utils/public/state_sync/index.md | 2 +- ...ic-state_sync.ikbnurlstatestorage.flush.md | 2 +- ...s-public-state_sync.ikbnurlstatestorage.md | 8 +- ...-state_sync.inullablebasestatecontainer.md | 2 +- ...-public-state_sync.istatestorage.cancel.md | 2 +- ...a_utils-public-state_sync.istatestorage.md | 2 +- ...-plugins-kibana_utils-public-state_sync.md | 12 +- ...ibana_utils-public-state_sync.syncstate.md | 16 +- .../audit_trail/audit_trail_service.mock.ts | 58 +++++++ .../audit_trail/audit_trail_service.test.ts | 99 ++++++++++++ .../server/audit_trail/audit_trail_service.ts | 69 ++++++++ src/core/server/audit_trail/index.ts | 21 +++ src/core/server/audit_trail/types.ts | 76 +++++++++ .../elasticsearch_service.test.ts | 1 + .../elasticsearch/elasticsearch_service.ts | 31 +++- .../legacy/cluster_client.test.ts | 148 +++++++++++++++--- .../elasticsearch/legacy/cluster_client.ts | 17 +- .../legacy/scoped_cluster_client.test.ts | 45 ++++++ .../legacy/scoped_cluster_client.ts | 19 ++- src/core/server/index.ts | 8 + src/core/server/internal_types.ts | 3 + src/core/server/legacy/legacy_service.test.ts | 3 +- src/core/server/legacy/legacy_service.ts | 2 + src/core/server/mocks.ts | 6 + src/core/server/plugins/plugin_context.ts | 2 + src/core/server/server.api.md | 41 ++++- src/core/server/server.test.mocks.ts | 6 + src/core/server/server.test.ts | 7 + src/core/server/server.ts | 14 +- x-pack/plugins/audit_trail/kibana.json | 10 ++ .../server/client/audit_trail_client.test.ts | 50 ++++++ .../server/client/audit_trail_client.ts | 46 ++++++ .../plugins/audit_trail/server/config.test.ts | 56 +++++++ x-pack/plugins/audit_trail/server/config.ts | 22 +++ x-pack/plugins/audit_trail/server/index.ts | 13 ++ .../plugins/audit_trail/server/plugin.test.ts | 125 +++++++++++++++ x-pack/plugins/audit_trail/server/plugin.ts | 97 ++++++++++++ x-pack/plugins/audit_trail/server/types.ts | 16 ++ x-pack/plugins/spaces/server/mocks.ts | 14 ++ x-pack/test/plugin_functional/config.ts | 7 + .../plugins/audit_trail_test/kibana.json | 9 ++ .../audit_trail_test/server/.gitignore | 1 + .../plugins/audit_trail_test/server/index.ts | 9 ++ .../plugins/audit_trail_test/server/plugin.ts | 65 ++++++++ .../test_suites/audit_trail/index.ts | 129 +++++++++++++++ 77 files changed, 1625 insertions(+), 76 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.add.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.auditorfactory.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailstart.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.httpserverinfo.hostname.md create mode 100644 src/core/server/audit_trail/audit_trail_service.mock.ts create mode 100644 src/core/server/audit_trail/audit_trail_service.test.ts create mode 100644 src/core/server/audit_trail/audit_trail_service.ts create mode 100644 src/core/server/audit_trail/index.ts create mode 100644 src/core/server/audit_trail/types.ts create mode 100644 x-pack/plugins/audit_trail/kibana.json create mode 100644 x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts create mode 100644 x-pack/plugins/audit_trail/server/client/audit_trail_client.ts create mode 100644 x-pack/plugins/audit_trail/server/config.test.ts create mode 100644 x-pack/plugins/audit_trail/server/config.ts create mode 100644 x-pack/plugins/audit_trail/server/index.ts create mode 100644 x-pack/plugins/audit_trail/server/plugin.test.ts create mode 100644 x-pack/plugins/audit_trail/server/plugin.ts create mode 100644 x-pack/plugins/audit_trail/server/types.ts create mode 100644 x-pack/plugins/spaces/server/mocks.ts create mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json create mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore create mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts create mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts create mode 100644 x-pack/test/plugin_functional/test_suites/audit_trail/index.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.md new file mode 100644 index 0000000000000..aa109c5064887 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditableevent.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) + +## AuditableEvent interface + +Event to audit. + +Signature: + +```typescript +export interface AuditableEvent +``` + +## Remarks + +Not a complete interface. + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-core-server.auditableevent.message.md) | string | | +| [type](./kibana-plugin-core-server.auditableevent.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md new file mode 100644 index 0000000000000..3ac4167c6998b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [message](./kibana-plugin-core-server.auditableevent.message.md) + +## AuditableEvent.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md new file mode 100644 index 0000000000000..3748748366684 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [type](./kibana-plugin-core-server.auditableevent.type.md) + +## AuditableEvent.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.add.md b/docs/development/core/server/kibana-plugin-core-server.auditor.add.md new file mode 100644 index 0000000000000..40245a93753fc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditor.add.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [add](./kibana-plugin-core-server.auditor.add.md) + +## Auditor.add() method + +Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents + +Signature: + +```typescript +add(event: AuditableEvent): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | AuditableEvent | | + +Returns: + +`void` + +## Example + +How to add a record in audit log: + +```typescript +router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { + context.core.auditor.withAuditScope('my_plugin_operation'); + const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); + context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.md b/docs/development/core/server/kibana-plugin-core-server.auditor.md new file mode 100644 index 0000000000000..191a34df647ab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditor.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) + +## Auditor interface + +Provides methods to log user actions and access events. + +Signature: + +```typescript +export interface Auditor +``` + +## Methods + +| Method | Description | +| --- | --- | +| [add(event)](./kibana-plugin-core-server.auditor.add.md) | Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents | +| [withAuditScope(name)](./kibana-plugin-core-server.auditor.withauditscope.md) | Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md b/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md new file mode 100644 index 0000000000000..0ae0c48ab92f4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [withAuditScope](./kibana-plugin-core-server.auditor.withauditscope.md) + +## Auditor.withAuditScope() method + +Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. + +Signature: + +```typescript +withAuditScope(name: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md new file mode 100644 index 0000000000000..4a60931e60940 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) > [asScoped](./kibana-plugin-core-server.auditorfactory.asscoped.md) + +## AuditorFactory.asScoped() method + +Signature: + +```typescript +asScoped(request: KibanaRequest): Auditor; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | KibanaRequest | | + +Returns: + +`Auditor` + diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md new file mode 100644 index 0000000000000..fd4760caa3552 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) + +## AuditorFactory interface + +Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. + +Signature: + +```typescript +export interface AuditorFactory +``` + +## Methods + +| Method | Description | +| --- | --- | +| [asScoped(request)](./kibana-plugin-core-server.auditorfactory.asscoped.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md new file mode 100644 index 0000000000000..50885232a088e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) + +## AuditTrailSetup interface + +Signature: + +```typescript +export interface AuditTrailSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [register(auditor)](./kibana-plugin-core-server.audittrailsetup.register.md) | Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md new file mode 100644 index 0000000000000..36695844ced73 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) > [register](./kibana-plugin-core-server.audittrailsetup.register.md) + +## AuditTrailSetup.register() method + +Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. + +Signature: + +```typescript +register(auditor: AuditorFactory): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| auditor | AuditorFactory | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md b/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md new file mode 100644 index 0000000000000..4fb9f5cb93549 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) + +## AuditTrailStart type + +Signature: + +```typescript +export declare type AuditTrailStart = AuditorFactory; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md new file mode 100644 index 0000000000000..1aa7a75b7a086 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [auditTrail](./kibana-plugin-core-server.coresetup.audittrail.md) + +## CoreSetup.auditTrail property + +[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) + +Signature: + +```typescript +auditTrail: AuditTrailSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 32221a320d2a1..597bb9bc2376a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetupAuditTrailSetup | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [context](./kibana-plugin-core-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md new file mode 100644 index 0000000000000..879e0df836190 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) + +## CoreStart.auditTrail property + +[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) + +Signature: + +```typescript +auditTrail: AuditTrailStart; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index acd23f0f47386..610c85c71e362 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -16,6 +16,7 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | +| [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) | AuditTrailStart | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.hostname.md b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.hostname.md new file mode 100644 index 0000000000000..194a8aea16269 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.hostname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) > [hostname](./kibana-plugin-core-server.httpserverinfo.hostname.md) + +## HttpServerInfo.hostname property + +The hostname of the server + +Signature: + +```typescript +hostname: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md index 823f34bd7dd23..6a56d31bbd55f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyClusterClient` class Signature: ```typescript -constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); +constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); ``` ## Parameters @@ -18,5 +18,6 @@ constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders | --- | --- | --- | | config | LegacyElasticsearchClientConfig | | | log | Logger | | +| getAuditorFactory | () => AuditorFactory | | | getAuthHeaders | GetAuthHeaders | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index 4f218ae552c99..c51f1858c97a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -15,7 +15,7 @@ export declare class LegacyClusterClient implements ILegacyClusterClient | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | +| [(constructor)(config, log, getAuditorFactory, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | ## Properties diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md index bd1cd1e9f3d9b..ffadab7656602 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyScopedClusterClient` class Signature: ```typescript -constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); +constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); ``` ## Parameters @@ -19,4 +19,5 @@ constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller | internalAPICaller | LegacyAPICaller | | | scopedAPICaller | LegacyAPICaller | | | headers | Headers | undefined | | +| auditor | Auditor | undefined | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index f3d8a69b8ed05..c4a94d8661c47 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -15,7 +15,7 @@ export declare class LegacyScopedClusterClient implements ILegacyScopedClusterCl | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | +| [(constructor)(internalAPICaller, scopedAPICaller, headers, auditor)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index f73595ea0a8ff..7ebd0531619fd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -56,6 +56,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) | Event to audit. | +| [Auditor](./kibana-plugin-core-server.auditor.md) | Provides methods to log user actions and access events. | +| [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) | Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. | +| [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | @@ -212,6 +216,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | | [AppenderConfigType](./kibana-plugin-core-server.appenderconfigtype.md) | | +| [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) | | | [AuthenticationHandler](./kibana-plugin-core-server.authenticationhandler.md) | See [AuthToolkit](./kibana-plugin-core-server.authtoolkit.md). | | [AuthHeaders](./kibana-plugin-core-server.authheaders.md) | Auth Headers map | | [AuthResult](./kibana-plugin-core-server.authresult.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index b09fb121b8a63..2d31c24a077cb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -20,5 +20,6 @@ core: { uiSettings: { client: IUiSettingsClient; }; + auditor: Auditor; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 55d6e931ac158..07e6dcbdae125 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md index 12af33756fb19..f429866848aa4 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md @@ -4,7 +4,7 @@ ## Comparator type -Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) +Used to compare state, see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md). Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md index e05f1fb392fe6..ca68c47ddaa7e 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md @@ -4,7 +4,7 @@ ## Connect type -Similar to `connect` from react-redux, allows to map state from state container to component's props +Similar to `connect` from react-redux, allows to map state from state container to component's props. Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md index 794bf63588312..8aadd0a234a8a 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md @@ -4,7 +4,7 @@ ## createStateContainer() function -Creates a state container with transitions, but without selectors +Creates a state container with transitions, but without selectors. Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md index 1946baae202f1..bb06ca18e808a 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md @@ -4,7 +4,7 @@ ## createStateContainer() function -Creates a state container with transitions and selectors +Creates a state container with transitions and selectors. Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md index 4f772c7c54d08..0b05775ad1458 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md @@ -4,7 +4,7 @@ ## CreateStateContainerOptions.freeze property -Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. +Function to use when freezing state. Supply identity function. If not provided, default `deepFreeze` is used. Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md index d328d306e93e1..8dba1b647edf4 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md @@ -16,5 +16,5 @@ export interface CreateStateContainerOptions | Property | Type | Description | | --- | --- | --- | -| [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) | <T>(state: T) => T | Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. | +| [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) | <T>(state: T) => T | Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is used. | diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md index e74ff2c6885be..7cabb72cecb31 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md @@ -11,8 +11,8 @@ State containers are Redux-store-like objects meant to help you manage state in | Function | Description | | --- | --- | | [createStateContainer(defaultState)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) | Creates a state container without transitions and without selectors. | -| [createStateContainer(defaultState, pureTransitions)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) | Creates a state container with transitions, but without selectors | -| [createStateContainer(defaultState, pureTransitions, pureSelectors, options)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) | Creates a state container with transitions and selectors | +| [createStateContainer(defaultState, pureTransitions)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) | Creates a state container with transitions, but without selectors. | +| [createStateContainer(defaultState, pureTransitions, pureSelectors, options)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) | Creates a state container with transitions and selectors. | ## Interfaces @@ -20,8 +20,8 @@ State containers are Redux-store-like objects meant to help you manage state in | --- | --- | | [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | Base state container shape without transitions or selectors | | [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | State container options | -| [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) | Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries | -| [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | +| [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) | Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). Allows to use state container with redux libraries. | +| [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md). | ## Variables @@ -36,8 +36,8 @@ State containers are Redux-store-like objects meant to help you manage state in | Type Alias | Description | | --- | --- | | [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) | Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape | -| [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) | Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | -| [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | Similar to connect from react-redux, allows to map state from state container to component's props | +| [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) | Used to compare state, see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md). | +| [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | Similar to connect from react-redux, allows to map state from state container to component's props. | | [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) | Redux like dispatch | | [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) | | | [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) | | diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md index 0e08119c1eae4..1229f4c2998f8 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md @@ -4,7 +4,7 @@ ## ReduxLikeStateContainer interface -Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries +Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). Allows to use state container with redux libraries. Signature: diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md index 23ec1c8e5be01..5d47540c824b0 100644 --- a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md @@ -4,7 +4,7 @@ ## StateContainer interface -Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) +Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md). Signature: diff --git a/docs/development/plugins/kibana_utils/public/state_sync/index.md b/docs/development/plugins/kibana_utils/public/state_sync/index.md index 4b345d9130bd5..5625e4a4b5eb8 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/index.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/index.md @@ -8,5 +8,5 @@ | Package | Description | | --- | --- | -| [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) | State syncing utilities are a set of helpers for syncing your application state with URL or browser storage.They are designed to work together with state containers (). But state containers are not required.State syncing utilities include:- util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with syncState: - - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - - Serializes state and persists it to browser storage.Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | +| [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) | State syncing utilities are a set of helpers for syncing your application state with browser URL or browser storage.They are designed to work together with [state containers](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers). But state containers are not required.State syncing utilities include:\* util which: \* Subscribes to state changes and pushes them to state storage. \* Optionally subscribes to state storage changes and pushes them to state. \* Two types of storages compatible with syncState: \* - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. \* - Serializes state and persists it to browser storage.Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples. | diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md index e0e6aa9be4368..dfeef1cdce22c 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md @@ -4,7 +4,7 @@ ## IKbnUrlStateStorage.flush property -synchronously runs any pending url updates returned boolean indicates if change occurred +Synchronously runs any pending url updates, returned boolean indicates if change occurred. Signature: diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md index 56cefebd2acfe..371f7b7c15362 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md @@ -4,7 +4,11 @@ ## IKbnUrlStateStorage interface -KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) +KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: + +1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See Kibana's `state:storeInSessionStorage` advanced option for more context. 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. + +[Refer to this guide for more info](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) Signature: @@ -18,7 +22,7 @@ export interface IKbnUrlStateStorage extends IStateStorage | --- | --- | --- | | [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | () => void | cancels any pending url updates | | [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | | -| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | synchronously runs any pending url updates returned boolean indicates if change occurred | +| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | Synchronously runs any pending url updates, returned boolean indicates if change occurred. | | [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <State = unknown>(key: string) => State | null | | | [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <State>(key: string, state: State, opts?: {
replace: boolean;
}) => Promise<string | undefined> | | diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md index ca69609936405..d81694484c3c0 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md @@ -14,7 +14,7 @@ export interface INullableBaseStateContainer extends Ba ## Remarks -State container for stateSync() have to accept "null" for example, set() implementation could handle null and fallback to some default state this is required to handle edge case, when state in storage becomes empty and syncing is in progress. state container will be notified about about storage becoming empty with null passed in +State container for `stateSync()` have to accept `null` for example, `set()` implementation could handle null and fallback to some default state this is required to handle edge case, when state in storage becomes empty and syncing is in progress. State container will be notified about about storage becoming empty with null passed in. ## Properties diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md index ce771d52a6e60..13bacfae9ef56 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md @@ -4,7 +4,7 @@ ## IStateStorage.cancel property -Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage +Optional method to cancel any pending activity [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) will call it during destroy, if it is provided by IStateStorage Signature: diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md index 2c34a185fb7b1..82f7949dfdc03 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md @@ -18,7 +18,7 @@ export interface IStateStorage | Property | Type | Description | | --- | --- | --- | -| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) | () => void | Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) | () => void | Optional method to cancel any pending activity [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) will call it during destroy, if it is provided by IStateStorage | | [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | Should notify when the stored state has changed | | [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) | <State = unknown>(key: string) => State | null | Should retrieve state from the storage and deserialize it | | [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) | <State>(key: string, state: State) => any | Take in a state object, should serialise and persist | diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md index 2b02c98e0d605..52919f78a035c 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md @@ -4,28 +4,28 @@ ## kibana-plugin-plugins-kibana\_utils-public-state\_sync package -State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. +State syncing utilities are a set of helpers for syncing your application state with browser URL or browser storage. -They are designed to work together with state containers (). But state containers are not required. +They are designed to work together with [state containers](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers). But state containers are not required. State syncing utilities include: -- [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with `syncState`: - [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) - Serializes state and persists it to browser storage. +\*[syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) util which: \* Subscribes to state changes and pushes them to state storage. \* Optionally subscribes to state storage changes and pushes them to state. \* Two types of storages compatible with `syncState`: \* [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. \* [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) - Serializes state and persists it to browser storage. -Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples +Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples. ## Functions | Function | Description | | --- | --- | -| [syncState({ storageKey, stateStorage, stateContainer, })](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) | Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | +| [syncState({ storageKey, stateStorage, stateContainer, })](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) | Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL)Go [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples. | | [syncStates(stateSyncConfigs)](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) | | ## Interfaces | Interface | Description | | --- | --- | -| [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) | KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) | +| [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) | KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which:1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See Kibana's state:storeInSessionStorage advanced option for more context. 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records.[Refer to this guide for more info](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) | | [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) | Extension of with one constraint: set state should handle null as incoming state | | [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) | [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | | [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) | Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storageFor an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages | diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md index d095c3fffc512..10dc4d0e18746 100644 --- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md @@ -4,7 +4,9 @@ ## syncState() function -Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples +Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) + +Go [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples. Signature: @@ -24,13 +26,9 @@ export declare function syncState ({ tab: s.tab }); diff --git a/src/core/server/audit_trail/audit_trail_service.mock.ts b/src/core/server/audit_trail/audit_trail_service.mock.ts new file mode 100644 index 0000000000000..d63b3539e5cdc --- /dev/null +++ b/src/core/server/audit_trail/audit_trail_service.mock.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AuditTrailSetup, AuditTrailStart, Auditor } from './types'; +import { AuditTrailService } from './audit_trail_service'; + +const createSetupContractMock = () => { + const mocked: jest.Mocked = { + register: jest.fn(), + }; + return mocked; +}; + +const createAuditorMock = () => { + const mocked: jest.Mocked = { + add: jest.fn(), + withAuditScope: jest.fn(), + }; + return mocked; +}; + +const createStartContractMock = () => { + const mocked: jest.Mocked = { + asScoped: jest.fn(), + }; + mocked.asScoped.mockReturnValue(createAuditorMock()); + return mocked; +}; + +const createServiceMock = (): jest.Mocked> => ({ + setup: jest.fn().mockResolvedValue(createSetupContractMock()), + start: jest.fn().mockResolvedValue(createStartContractMock()), + stop: jest.fn(), +}); + +export const auditTrailServiceMock = { + create: createServiceMock, + createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, + createAuditorFactory: createStartContractMock, + createAuditor: createAuditorMock, +}; diff --git a/src/core/server/audit_trail/audit_trail_service.test.ts b/src/core/server/audit_trail/audit_trail_service.test.ts new file mode 100644 index 0000000000000..63b45b62275b6 --- /dev/null +++ b/src/core/server/audit_trail/audit_trail_service.test.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AuditTrailService } from './audit_trail_service'; +import { AuditorFactory } from './types'; +import { mockCoreContext } from '../core_context.mock'; +import { httpServerMock } from '../http/http_server.mocks'; + +describe('AuditTrailService', () => { + const coreContext = mockCoreContext.create(); + + describe('#setup', () => { + describe('register', () => { + it('throws if registered the same auditor factory twice', () => { + const auditTrail = new AuditTrailService(coreContext); + const { register } = auditTrail.setup(); + const auditorFactory: AuditorFactory = { + asScoped() { + return { add: () => undefined, withAuditScope: (() => {}) as any }; + }, + }; + register(auditorFactory); + expect(() => register(auditorFactory)).toThrowErrorMatchingInlineSnapshot( + `"An auditor factory has been already registered"` + ); + }); + }); + }); + + describe('#start', () => { + describe('asScoped', () => { + it('initialize every auditor with a request', () => { + const scopedMock = jest.fn(() => ({ add: jest.fn(), withAuditScope: jest.fn() })); + const auditorFactory = { asScoped: scopedMock }; + + const auditTrail = new AuditTrailService(coreContext); + const { register } = auditTrail.setup(); + register(auditorFactory); + + const { asScoped } = auditTrail.start(); + const kibanaRequest = httpServerMock.createKibanaRequest(); + asScoped(kibanaRequest); + + expect(scopedMock).toHaveBeenCalledWith(kibanaRequest); + }); + + it('passes auditable event to an auditor', () => { + const addEventMock = jest.fn(); + const auditorFactory = { + asScoped() { + return { add: addEventMock, withAuditScope: jest.fn() }; + }, + }; + + const auditTrail = new AuditTrailService(coreContext); + const { register } = auditTrail.setup(); + register(auditorFactory); + + const { asScoped } = auditTrail.start(); + const kibanaRequest = httpServerMock.createKibanaRequest(); + const auditor = asScoped(kibanaRequest); + const message = { + type: 'foo', + message: 'bar', + }; + auditor.add(message); + + expect(addEventMock).toHaveBeenLastCalledWith(message); + }); + + describe('return the same auditor instance for the same KibanaRequest', () => { + const auditTrail = new AuditTrailService(coreContext); + auditTrail.setup(); + const { asScoped } = auditTrail.start(); + + const rawRequest1 = httpServerMock.createKibanaRequest(); + const rawRequest2 = httpServerMock.createKibanaRequest(); + expect(asScoped(rawRequest1)).toBe(asScoped(rawRequest1)); + expect(asScoped(rawRequest1)).not.toBe(asScoped(rawRequest2)); + }); + }); + }); +}); diff --git a/src/core/server/audit_trail/audit_trail_service.ts b/src/core/server/audit_trail/audit_trail_service.ts new file mode 100644 index 0000000000000..f1841858dbc92 --- /dev/null +++ b/src/core/server/audit_trail/audit_trail_service.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { KibanaRequest, LegacyRequest } from '../http'; +import { ensureRawRequest } from '../http/router'; +import { Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; + +const defaultAuditorFactory: AuditorFactory = { + asScoped() { + return { + add() {}, + withAuditScope() {}, + }; + }, +}; + +export class AuditTrailService implements CoreService { + private readonly log: Logger; + private auditor: AuditorFactory = defaultAuditorFactory; + private readonly auditors = new WeakMap(); + + constructor(core: CoreContext) { + this.log = core.logger.get('audit_trail'); + } + + setup() { + return { + register: (auditor: AuditorFactory) => { + if (this.auditor !== defaultAuditorFactory) { + throw new Error('An auditor factory has been already registered'); + } + this.auditor = auditor; + this.log.debug('An auditor factory has been registered'); + }, + }; + } + + start() { + return { + asScoped: (request: KibanaRequest) => { + const key = ensureRawRequest(request); + if (!this.auditors.has(key)) { + this.auditors.set(key, this.auditor!.asScoped(request)); + } + return this.auditors.get(key)!; + }, + }; + } + + stop() {} +} diff --git a/src/core/server/audit_trail/index.ts b/src/core/server/audit_trail/index.ts new file mode 100644 index 0000000000000..3f01e6fa3582d --- /dev/null +++ b/src/core/server/audit_trail/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { AuditTrailService } from './audit_trail_service'; +export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; diff --git a/src/core/server/audit_trail/types.ts b/src/core/server/audit_trail/types.ts new file mode 100644 index 0000000000000..b3c1fc3c222fa --- /dev/null +++ b/src/core/server/audit_trail/types.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { KibanaRequest } from '../http'; + +/** + * Event to audit. + * @public + * + * @remarks + * Not a complete interface. + */ +export interface AuditableEvent { + message: string; + type: string; +} + +/** + * Provides methods to log user actions and access events. + * @public + */ +export interface Auditor { + /** + * Add a record to audit log. + * Service attaches to a log record: + * - metadata about an end-user initiating an operation + * - scope name, if presents + * + * @example + * How to add a record in audit log: + * ```typescript + * router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { + * context.core.auditor.withAuditScope('my_plugin_operation'); + * const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); + * context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); + * ``` + */ + add(event: AuditableEvent): void; + /** + * Add a high-level scope name for logged events. + * It helps to identify the root cause of low-level events. + */ + withAuditScope(name: string): void; +} + +/** + * Creates {@link Auditor} instance bound to the current user credentials. + * @public + */ +export interface AuditorFactory { + asScoped(request: KibanaRequest): Auditor; +} + +export interface AuditTrailSetup { + /** + * Register a custom {@link AuditorFactory} implementation. + */ + register(auditor: AuditorFactory): void; +} + +export type AuditTrailStart = AuditorFactory; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 99d12b8662577..8f3dc5688f6fc 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -101,6 +101,7 @@ describe('#setup', () => { expect(MockClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), + expect.any(Function), expect.any(Function) ); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index f47b33dd410f6..4ea10f6ae4e2e 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -42,6 +42,7 @@ import { } from './legacy'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; +import { AuditTrailStart, AuditorFactory } from '../audit_trail'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceStart, @@ -60,12 +61,17 @@ interface SetupDeps { http: InternalHttpServiceSetup; } +interface StartDeps { + auditTrail: AuditTrailStart; +} + /** @internal */ export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; private subscription?: Subscription; + private auditorFactory?: AuditorFactory; private stop$ = new Subject(); private kibanaVersion: string; private createClient?: ( @@ -132,14 +138,24 @@ export class ElasticsearchService return await _client.callAsInternalUser(endpoint, clientParams, options); }, asScoped(request: ScopeableRequest) { + const _clientPromise = client$.pipe(take(1)).toPromise(); return { - callAsInternalUser: client.callAsInternalUser, + async callAsInternalUser( + endpoint: string, + clientParams: Record = {}, + options?: LegacyCallAPIOptions + ) { + const _client = await _clientPromise; + return await _client + .asScoped(request) + .callAsInternalUser(endpoint, clientParams, options); + }, async callAsCurrentUser( endpoint: string, clientParams: Record = {}, options?: LegacyCallAPIOptions ) { - const _client = await client$.pipe(take(1)).toPromise(); + const _client = await _clientPromise; return await _client .asScoped(request) .callAsCurrentUser(endpoint, clientParams, options); @@ -176,7 +192,8 @@ export class ElasticsearchService status$: calculateStatus$(esNodesCompatibility$), }; } - public async start() { + public async start({ auditTrail }: StartDeps) { + this.auditorFactory = auditTrail; if (typeof this.client === 'undefined' || typeof this.createClient === 'undefined') { throw new Error('ElasticsearchService needs to be setup before calling start'); } else { @@ -205,7 +222,15 @@ export class ElasticsearchService return new LegacyClusterClient( config, this.coreContext.logger.get('elasticsearch', type), + this.getAuditorFactory, getAuthHeaders ); } + + private getAuditorFactory = () => { + if (!this.auditorFactory) { + throw new Error('auditTrail has not been initialized'); + } + return this.auditorFactory; + }; } diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 9ab4862a1ab4c..2f0f80728c707 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -27,6 +27,7 @@ import { import { errors } from 'elasticsearch'; import { get } from 'lodash'; +import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; import { Logger } from '../../logging'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { httpServerMock } from '../../http/http_server.mocks'; @@ -42,7 +43,11 @@ test('#constructor creates client with parsed config', () => { const mockEsConfig = { apiVersion: 'es-version' } as any; const mockLogger = logger.get(); - const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + const clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); expect(clusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); @@ -68,7 +73,11 @@ describe('#callAsInternalUser', () => { }; MockClient.mockImplementation(() => mockEsClientInstance); - clusterClient = new LegacyClusterClient({ apiVersion: 'es-version' } as any, logger.get()); + clusterClient = new LegacyClusterClient( + { apiVersion: 'es-version' } as any, + logger.get(), + auditTrailServiceMock.createAuditorFactory + ); }); test('fails if cluster client is closed', async () => { @@ -237,7 +246,11 @@ describe('#asScoped', () => { requestHeadersWhitelist: ['one', 'two'], } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); jest.clearAllMocks(); }); @@ -272,7 +285,11 @@ describe('#asScoped', () => { test('properly configures `ignoreCertAndKey` for various configurations', () => { // Config without SSL. - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -285,7 +302,11 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === false mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -298,7 +319,11 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === true mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -319,7 +344,8 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' } + { one: '1', two: '2' }, + expect.any(Object) ); }); @@ -341,71 +367,142 @@ describe('#asScoped', () => { }); test('does not fail when scope to not defined request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); clusterClient.asScoped(); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {} + {}, + undefined ); }); test('does not fail when scope to a request without headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); clusterClient.asScoped({} as any); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {} + {}, + undefined ); }); test('calls getAuthHeaders and filters results for a real request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ - one: '1', - three: '3', - })); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory, + () => ({ + one: '1', + three: '3', + }) + ); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' } + { one: '1', two: '2' }, + expect.any(Object) ); }); test('getAuthHeaders results rewrite extends a request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ one: 'foo' })); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory, + () => ({ one: 'foo' }) + ); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: 'foo', two: '2' } + { one: 'foo', two: '2' }, + expect.any(Object) ); }); test("doesn't call getAuthHeaders for a fake request", async () => { - const getAuthHeaders = jest.fn(); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); - clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory, + () => ({}) + ); + clusterClient.asScoped({ headers: { one: 'foo' } }); - expect(getAuthHeaders).not.toHaveBeenCalled(); + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + { one: 'foo' }, + undefined + ); }); test('filters a fake request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); + clusterClient = new LegacyClusterClient( + mockEsConfig, + mockLogger, + auditTrailServiceMock.createAuditorFactory + ); clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' } + { one: '1', two: '2' }, + undefined ); }); + + describe('Auditor', () => { + it('creates Auditor for KibanaRequest', async () => { + const auditor = auditTrailServiceMock.createAuditor(); + const auditorFactory = auditTrailServiceMock.createAuditorFactory(); + auditorFactory.asScoped.mockReturnValue(auditor); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => auditorFactory); + clusterClient.asScoped(httpServerMock.createKibanaRequest()); + + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + {}, + auditor + ); + }); + + it("doesn't create Auditor for a fake request", async () => { + const getAuthHeaders = jest.fn(); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); + clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); + + expect(getAuthHeaders).not.toHaveBeenCalled(); + }); + + it("doesn't create Auditor when no request passed", async () => { + const getAuthHeaders = jest.fn(); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); + clusterClient.asScoped(); + + expect(getAuthHeaders).not.toHaveBeenCalled(); + }); + }); }); describe('#close', () => { @@ -423,7 +520,8 @@ describe('#close', () => { clusterClient = new LegacyClusterClient( { apiVersion: 'es-version', requestHeadersWhitelist: [] } as any, - logger.get() + logger.get(), + auditTrailServiceMock.createAuditorFactory ); }); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 8e2d20a8972fc..7a39113d25a14 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -20,7 +20,8 @@ import { Client } from 'elasticsearch'; import { get } from 'lodash'; import { LegacyElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isRealRequest } from '../../http'; +import { GetAuthHeaders, isRealRequest, KibanaRequest } from '../../http'; +import { AuditorFactory } from '../../audit_trail'; import { filterHeaders, ensureRawRequest } from '../../http/router'; import { Logger } from '../../logging'; import { ScopeableRequest } from '../types'; @@ -129,6 +130,7 @@ export class LegacyClusterClient implements ILegacyClusterClient { constructor( private readonly config: LegacyElasticsearchClientConfig, private readonly log: Logger, + private readonly getAuditorFactory: () => AuditorFactory, private readonly getAuthHeaders: GetAuthHeaders = noop ) { this.client = new Client(parseElasticsearchClientConfig(config, log)); @@ -203,10 +205,21 @@ export class LegacyClusterClient implements ILegacyClusterClient { return new LegacyScopedClusterClient( this.callAsInternalUser, this.callAsCurrentUser, - filterHeaders(this.getHeaders(request), this.config.requestHeadersWhitelist) + filterHeaders(this.getHeaders(request), this.config.requestHeadersWhitelist), + this.getScopedAuditor(request) ); } + private getScopedAuditor(request?: ScopeableRequest) { + // TODO: support alternative credential owners from outside of Request context in #39430 + if (request && isRealRequest(request)) { + const kibanaRequest = + request instanceof KibanaRequest ? request : KibanaRequest.from(request); + const auditorFactory = this.getAuditorFactory(); + return auditorFactory.asScoped(kibanaRequest); + } + } + /** * Calls specified endpoint with provided clientParams on behalf of the * user initiated request to the Kibana server (via HTTP request headers). diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts index 6275832faec9b..234a46c94d43d 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts @@ -18,6 +18,7 @@ */ import { LegacyScopedClusterClient } from './scoped_cluster_client'; +import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; let internalAPICaller: jest.Mock; let scopedAPICaller: jest.Mock; @@ -83,6 +84,28 @@ describe('#callAsInternalUser', () => { expect(scopedAPICaller).not.toHaveBeenCalled(); }); + + describe('Auditor', () => { + it('does not fail when no auditor provided', () => { + const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); + expect(() => clusterClientWithoutAuditor.callAsInternalUser('endpoint')).not.toThrow(); + }); + it('creates an audit record if auditor provided', () => { + const auditor = auditTrailServiceMock.createAuditor(); + const clusterClientWithoutAuditor = new LegacyScopedClusterClient( + jest.fn(), + jest.fn(), + {}, + auditor + ); + clusterClientWithoutAuditor.callAsInternalUser('endpoint'); + expect(auditor.add).toHaveBeenCalledTimes(1); + expect(auditor.add).toHaveBeenLastCalledWith({ + message: 'endpoint', + type: 'elasticsearch.call.internalUser', + }); + }); + }); }); describe('#callAsCurrentUser', () => { @@ -194,4 +217,26 @@ describe('#callAsCurrentUser', () => { expect(internalAPICaller).not.toHaveBeenCalled(); }); + + describe('Auditor', () => { + it('does not fail when no auditor provided', () => { + const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); + expect(() => clusterClientWithoutAuditor.callAsCurrentUser('endpoint')).not.toThrow(); + }); + it('creates an audit record if auditor provided', () => { + const auditor = auditTrailServiceMock.createAuditor(); + const clusterClientWithoutAuditor = new LegacyScopedClusterClient( + jest.fn(), + jest.fn(), + {}, + auditor + ); + clusterClientWithoutAuditor.callAsCurrentUser('endpoint'); + expect(auditor.add).toHaveBeenCalledTimes(1); + expect(auditor.add).toHaveBeenLastCalledWith({ + message: 'endpoint', + type: 'elasticsearch.call.currentUser', + }); + }); + }); }); diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index e62409ff00c7e..9edb73645f0e2 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -18,6 +18,7 @@ */ import { intersection, isObject } from 'lodash'; +import { Auditor } from '../../audit_trail'; import { Headers } from '../../http/router'; import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; @@ -44,7 +45,8 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor( private readonly internalAPICaller: LegacyAPICaller, private readonly scopedAPICaller: LegacyAPICaller, - private readonly headers?: Headers + private readonly headers?: Headers, + private readonly auditor?: Auditor ) { this.callAsCurrentUser = this.callAsCurrentUser.bind(this); this.callAsInternalUser = this.callAsInternalUser.bind(this); @@ -64,6 +66,13 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams: Record = {}, options?: LegacyCallAPIOptions ) { + if (this.auditor) { + this.auditor.add({ + message: endpoint, + type: 'elasticsearch.call.internalUser', + }); + } + return this.internalAPICaller(endpoint, clientParams, options); } @@ -95,6 +104,14 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams.headers = Object.assign({}, clientParams.headers, this.headers); } + + if (this.auditor) { + this.auditor.add({ + message: endpoint, + type: 'elasticsearch.call.currentUser', + }); + } + return this.scopedAPICaller(endpoint, clientParams, options); } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 35aabab4a0b26..dcaa5f2367214 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -62,6 +62,7 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; import { MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; +import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { LoggingServiceSetup, appendersSchema, @@ -69,6 +70,7 @@ import { loggerSchema, } from './logging'; +export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; export { @@ -376,6 +378,7 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; + auditor: Auditor; }; } @@ -412,6 +415,8 @@ export interface CoreSetup; + /** {@link AuditTrailSetup} */ + auditTrail: AuditTrailSetup; } /** @@ -445,6 +450,8 @@ export interface CoreStart { savedObjects: SavedObjectsServiceStart; /** {@link UiSettingsServiceStart} */ uiSettings: UiSettingsServiceStart; + /** {@link AuditTrailSetup} */ + auditTrail: AuditTrailStart; } export { @@ -456,6 +463,7 @@ export { PluginsServiceStart, PluginOpaqueId, UuidServiceSetup, + AuditTrailStart, }; /** diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index f6c17c2379862..24080f2529beb 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -34,6 +34,7 @@ import { InternalMetricsServiceStart } from './metrics'; import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; +import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; /** @internal */ @@ -48,6 +49,7 @@ export interface InternalCoreSetup { uuid: UuidServiceSetup; rendering: InternalRenderingServiceSetup; httpResources: InternalHttpResourcesSetup; + auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; } @@ -61,6 +63,7 @@ export interface InternalCoreStart { metrics: InternalMetricsServiceStart; savedObjects: InternalSavedObjectsServiceStart; uiSettings: InternalUiSettingsServiceStart; + auditTrail: AuditTrailStart; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index ffe3b2375bc90..ae4cf612e92ce 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -51,6 +51,7 @@ import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './ty import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; import { statusServiceMock } from '../status/status_service.mock'; +import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -98,6 +99,7 @@ beforeEach(() => { rendering: renderingServiceMock, uuid: uuidSetup, status: statusServiceMock.createInternalSetupContract(), + auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, @@ -119,7 +121,6 @@ beforeEach(() => { startDeps = { core: { ...coreMock.createInternalStart(), - savedObjects: savedObjectsServiceMock.createInternalStartContract(), plugins: { contracts: new Map() }, }, plugins: {}, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index a544bad6c0e41..6b34a4eb58319 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -280,6 +280,7 @@ export class LegacyService implements CoreService { getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, + auditTrail: startDeps.core.auditTrail, }; const router = setupDeps.core.http.createRouter('', this.legacyId); @@ -330,6 +331,7 @@ export class LegacyService implements CoreService { uuid: { getInstanceUuid: setupDeps.core.uuid.getInstanceUuid, }, + auditTrail: setupDeps.core.auditTrail, getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 73d8e79069ce3..75ca88627814b 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -36,6 +36,7 @@ import { capabilitiesServiceMock } from './capabilities/capabilities_service.moc import { metricsServiceMock } from './metrics/metrics_service.mock'; import { uuidServiceMock } from './uuid/uuid_service.mock'; import { statusServiceMock } from './status/status_service.mock'; +import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { httpResourcesMock } from './http_resources/http_resources_service.mock'; @@ -131,6 +132,7 @@ function createCoreSetupMock({ status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), + auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() @@ -142,6 +144,7 @@ function createCoreSetupMock({ function createCoreStartMock() { const mock: MockedKeys = { + auditTrail: auditTrailServiceMock.createStartContract(), capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), @@ -165,6 +168,7 @@ function createInternalCoreSetupMock() { httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), }; return setupDeps; @@ -178,6 +182,7 @@ function createInternalCoreStartMock() { metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + auditTrail: auditTrailServiceMock.createStartContract(), }; return startDeps; } @@ -196,6 +201,7 @@ function createCoreRequestHandlerContextMock() { uiSettings: { client: uiSettingsServiceMock.createClient(), }, + auditor: auditTrailServiceMock.createAuditor(), }; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 4643789d99a88..b0f9ff6fd5ebd 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,6 +185,7 @@ export function createPluginSetupContext( getInstanceUuid: deps.uuid.getInstanceUuid, }, getStartServices: () => plugin.startDependencies, + auditTrail: deps.auditTrail, }; } @@ -228,5 +229,6 @@ export function createPluginStartContext( uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, + auditTrail: deps.auditTrail, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c2eade5f05ccd..ea95329bf8fa4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -170,6 +170,38 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public +export interface AuditableEvent { + // (undocumented) + message: string; + // (undocumented) + type: string; +} + +// @public +export interface Auditor { + add(event: AuditableEvent): void; + withAuditScope(name: string): void; +} + +// @public +export interface AuditorFactory { + // (undocumented) + asScoped(request: KibanaRequest): Auditor; +} + +// Warning: (ae-missing-release-tag) "AuditTrailSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface AuditTrailSetup { + register(auditor: AuditorFactory): void; +} + +// Warning: (ae-missing-release-tag) "AuditTrailStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AuditTrailStart = AuditorFactory; + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -439,6 +471,8 @@ export type CoreId = symbol; // @public export interface CoreSetup { + // (undocumented) + auditTrail: AuditTrailSetup; // (undocumented) capabilities: CapabilitiesSetup; // (undocumented) @@ -465,6 +499,8 @@ export interface CoreSetup AuditorFactory, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; callAsInternalUser: LegacyAPICaller; close(): void; @@ -1286,7 +1322,7 @@ export interface LegacyRequest extends Request { // // @public (undocumented) export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { - constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); + constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } @@ -1689,6 +1725,7 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; + auditor: Auditor; }; } diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index e5e710d54e04b..82d0c095bfe95 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -97,3 +97,9 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); + +import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; +export const mockAuditTrailService = auditTrailServiceMock.create(); +jest.doMock('./audit_trail/audit_trail_service', () => ({ + AuditTrailService: jest.fn(() => mockAuditTrailService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 1f507a85d3ddf..417f66a2988c2 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,6 +31,7 @@ import { mockMetricsService, mockStatusService, mockLoggingService, + mockAuditTrailService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -70,6 +71,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockAuditTrailService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -83,6 +85,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); + expect(mockAuditTrailService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -123,6 +126,7 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); + expect(mockAuditTrailService.start).not.toHaveBeenCalled(); await server.start(); @@ -131,6 +135,7 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); + expect(mockAuditTrailService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -155,6 +160,7 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).not.toHaveBeenCalled(); expect(mockStatusService.stop).not.toHaveBeenCalled(); expect(mockLoggingService.stop).not.toHaveBeenCalled(); + expect(mockAuditTrailService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -167,6 +173,7 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); expect(mockStatusService.stop).toHaveBeenCalledTimes(1); expect(mockLoggingService.stop).toHaveBeenCalledTimes(1); + expect(mockAuditTrailService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index dc37b77c57c92..86b8c550b2d9b 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -27,6 +27,7 @@ import { coreDeprecationProvider, } from './config'; import { CoreApp } from './core_app'; +import { AuditTrailService } from './audit_trail'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -76,6 +77,7 @@ export class Server { private readonly status: StatusService; private readonly logging: LoggingService; private readonly coreApp: CoreApp; + private readonly auditTrail: AuditTrailService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -105,6 +107,7 @@ export class Server { this.status = new StatusService(core); this.coreApp = new CoreApp(core); this.httpResources = new HttpResourcesService(core); + this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); } @@ -127,6 +130,7 @@ export class Server { pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), }); + const auditTrailSetup = this.auditTrail.setup(); const uuidSetup = await this.uuid.setup(); const httpSetup = await this.http.setup({ @@ -183,6 +187,7 @@ export class Server { uuid: uuidSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, + auditTrail: auditTrailSetup, logging: loggingSetup, }; @@ -203,7 +208,11 @@ export class Server { public async start() { this.log.debug('starting server'); - const elasticsearchStart = await this.elasticsearch.start(); + const auditTrailStart = this.auditTrail.start(); + + const elasticsearchStart = await this.elasticsearch.start({ + auditTrail: auditTrailStart, + }); const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, pluginsInitialized: this.#pluginsInitialized, @@ -220,6 +229,7 @@ export class Server { metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, + auditTrail: auditTrailStart, }; const pluginsStart = await this.plugins.start(this.coreStart); @@ -254,6 +264,7 @@ export class Server { await this.metrics.stop(); await this.status.stop(); await this.logging.stop(); + await this.auditTrail.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { @@ -277,6 +288,7 @@ export class Server { uiSettings: { client: coreStart.uiSettings.asScopedToClient(savedObjectsClient), }, + auditor: coreStart.auditTrail.asScoped(req), }; } ); diff --git a/x-pack/plugins/audit_trail/kibana.json b/x-pack/plugins/audit_trail/kibana.json new file mode 100644 index 0000000000000..ce92e232ec13b --- /dev/null +++ b/x-pack/plugins/audit_trail/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "auditTrail", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "audit_trail"], + "server": true, + "ui": false, + "requiredPlugins": ["licensing", "security"], + "optionalPlugins": ["spaces"] +} diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts new file mode 100644 index 0000000000000..cdc0aa4cfd7e7 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { Subject } from 'rxjs'; + +import { AuditTrailClient } from './audit_trail_client'; +import { AuditEvent } from '../types'; + +import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { spacesMock } from '../../../spaces/server/mocks'; + +describe('AuditTrailClient', () => { + let client: AuditTrailClient; + let event$: Subject; + const deps = { + getCurrentUser: securityMock.createSetup().authc.getCurrentUser, + getSpaceId: spacesMock.createSetup().spacesService.getSpaceId, + }; + + beforeEach(() => { + event$ = new Subject(); + client = new AuditTrailClient(httpServerMock.createKibanaRequest(), event$, deps); + }); + + afterEach(() => { + event$.complete(); + }); + + describe('#withAuditScope', () => { + it('registers upper level scope', (done) => { + client.withAuditScope('scope_name'); + event$.subscribe((event) => { + expect(event.scope).toBe('scope_name'); + done(); + }); + client.add({ message: 'message', type: 'type' }); + }); + + it('throws an exception if tries to re-write a scope', () => { + client.withAuditScope('scope_name'); + expect(() => client.withAuditScope('another_scope_name')).toThrowErrorMatchingInlineSnapshot( + `"Audit scope is already set to: scope_name"` + ); + }); + }); +}); diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts new file mode 100644 index 0000000000000..f12977cddaf0b --- /dev/null +++ b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts @@ -0,0 +1,46 @@ +/* + * 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 { Subject } from 'rxjs'; +import { KibanaRequest, Auditor, AuditableEvent } from 'src/core/server'; +import { AuditEvent } from '../types'; + +import { SecurityPluginSetup } from '../../../security/server'; +import { SpacesPluginSetup } from '../../../spaces/server'; + +interface Deps { + getCurrentUser: SecurityPluginSetup['authc']['getCurrentUser']; + getSpaceId?: SpacesPluginSetup['spacesService']['getSpaceId']; +} + +export class AuditTrailClient implements Auditor { + private scope?: string; + constructor( + private readonly request: KibanaRequest, + private readonly event$: Subject, + private readonly deps: Deps + ) {} + + public withAuditScope(name: string) { + if (this.scope !== undefined) { + throw new Error(`Audit scope is already set to: ${this.scope}`); + } + this.scope = name; + } + + public add(event: AuditableEvent) { + const user = this.deps.getCurrentUser(this.request); + // doesn't use getSpace since it's async operation calling ES + const spaceId = this.deps.getSpaceId ? this.deps.getSpaceId(this.request) : undefined; + + this.event$.next({ + message: event.message, + type: event.type, + user: user?.username, + space: spaceId, + scope: this.scope, + }); + } +} diff --git a/x-pack/plugins/audit_trail/server/config.test.ts b/x-pack/plugins/audit_trail/server/config.test.ts new file mode 100644 index 0000000000000..65dfc9f589ec9 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/config.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { config } from './config'; + +describe('config schema', () => { + it('generates proper defaults', () => { + expect(config.schema.validate({})).toEqual({ + enabled: false, + logger: { + enabled: false, + }, + }); + }); + + it('accepts an appender', () => { + const appender = config.schema.validate({ + appender: { + kind: 'file', + path: '/path/to/file.txt', + layout: { + kind: 'json', + }, + }, + logger: { + enabled: false, + }, + }).appender; + + expect(appender).toEqual({ + kind: 'file', + path: '/path/to/file.txt', + layout: { + kind: 'json', + }, + }); + }); + + it('rejects an appender if not fully configured', () => { + expect(() => + config.schema.validate({ + // no layout configured + appender: { + kind: 'file', + path: '/path/to/file.txt', + }, + logger: { + enabled: false, + }, + }) + ).toThrow(); + }); +}); diff --git a/x-pack/plugins/audit_trail/server/config.ts b/x-pack/plugins/audit_trail/server/config.ts new file mode 100644 index 0000000000000..7b05c04c2236f --- /dev/null +++ b/x-pack/plugins/audit_trail/server/config.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor, config as coreConfig } from '../../../../src/core/server'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + appender: schema.maybe(coreConfig.logging.appenders), + logger: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), +}); + +export type AuditTrailConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; diff --git a/x-pack/plugins/audit_trail/server/index.ts b/x-pack/plugins/audit_trail/server/index.ts new file mode 100644 index 0000000000000..7db48823a0e29 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { AuditTrailPlugin } from './plugin'; + +export { config } from './config'; +export const plugin = (initializerContext: PluginInitializerContext) => { + return new AuditTrailPlugin(initializerContext); +}; diff --git a/x-pack/plugins/audit_trail/server/plugin.test.ts b/x-pack/plugins/audit_trail/server/plugin.test.ts new file mode 100644 index 0000000000000..fa5fd1bcc1e14 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/plugin.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { AuditTrailPlugin } from './plugin'; +import { coreMock } from '../../../../src/core/server/mocks'; + +import { securityMock } from '../../security/server/mocks'; +import { spacesMock } from '../../spaces/server/mocks'; + +describe('AuditTrail plugin', () => { + describe('#setup', () => { + let plugin: AuditTrailPlugin; + let pluginInitContextMock: ReturnType; + let coreSetup: ReturnType; + + const deps = { + security: securityMock.createSetup(), + spaces: spacesMock.createSetup(), + }; + + beforeEach(() => { + pluginInitContextMock = coreMock.createPluginInitializerContext(); + plugin = new AuditTrailPlugin(pluginInitContextMock); + coreSetup = coreMock.createSetup(); + }); + + afterEach(async () => { + await plugin.stop(); + }); + + it('registers AuditTrail factory', async () => { + pluginInitContextMock = coreMock.createPluginInitializerContext(); + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + expect(coreSetup.auditTrail.register).toHaveBeenCalledTimes(1); + }); + + describe('logger', () => { + it('registers a custom logger', async () => { + pluginInitContextMock = coreMock.createPluginInitializerContext(); + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + + expect(coreSetup.logging.configure).toHaveBeenCalledTimes(1); + }); + + it('disables logging if config.logger.enabled: false', async () => { + const config = { + logger: { + enabled: false, + }, + }; + pluginInitContextMock = coreMock.createPluginInitializerContext(config); + + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + + const args = coreSetup.logging.configure.mock.calls[0][0]; + const value = await args.pipe(first()).toPromise(); + expect(value.loggers?.every((l) => l.level === 'off')).toBe(true); + }); + it('logs with DEBUG level if config.logger.enabled: true', async () => { + const config = { + logger: { + enabled: true, + }, + }; + pluginInitContextMock = coreMock.createPluginInitializerContext(config); + + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + + const args = coreSetup.logging.configure.mock.calls[0][0]; + const value = await args.pipe(first()).toPromise(); + expect(value.loggers?.every((l) => l.level === 'debug')).toBe(true); + }); + it('uses appender adjusted via config', async () => { + const config = { + appender: { + kind: 'file', + path: '/path/to/file.txt', + }, + logger: { + enabled: true, + }, + }; + pluginInitContextMock = coreMock.createPluginInitializerContext(config); + + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + + const args = coreSetup.logging.configure.mock.calls[0][0]; + const value = await args.pipe(first()).toPromise(); + expect(value.appenders).toEqual({ auditTrailAppender: config.appender }); + }); + it('falls back to the default appender if not configured', async () => { + const config = { + logger: { + enabled: true, + }, + }; + pluginInitContextMock = coreMock.createPluginInitializerContext(config); + + plugin = new AuditTrailPlugin(pluginInitContextMock); + plugin.setup(coreSetup, deps); + + const args = coreSetup.logging.configure.mock.calls[0][0]; + const value = await args.pipe(first()).toPromise(); + expect(value.appenders).toEqual({ + auditTrailAppender: { + kind: 'console', + layout: { + kind: 'pattern', + highlight: true, + }, + }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/audit_trail/server/plugin.ts b/x-pack/plugins/audit_trail/server/plugin.ts new file mode 100644 index 0000000000000..cf423f230aef9 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/plugin.ts @@ -0,0 +1,97 @@ +/* + * 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 { Observable, Subject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + AppenderConfigType, + CoreSetup, + CoreStart, + KibanaRequest, + Logger, + LoggerContextConfigInput, + Plugin, + PluginInitializerContext, +} from 'src/core/server'; + +import { AuditEvent } from './types'; +import { AuditTrailClient } from './client/audit_trail_client'; +import { AuditTrailConfigType } from './config'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { LicensingPluginStart } from '../../licensing/server'; + +interface DepsSetup { + security: SecurityPluginSetup; + spaces?: SpacesPluginSetup; +} + +interface DepStart { + licensing: LicensingPluginStart; +} + +export class AuditTrailPlugin implements Plugin { + private readonly logger: Logger; + private readonly config$: Observable; + private readonly event$ = new Subject(); + + constructor(private readonly context: PluginInitializerContext) { + this.logger = this.context.logger.get(); + this.config$ = this.context.config.create(); + } + + public setup(core: CoreSetup, deps: DepsSetup) { + const depsApi = { + getCurrentUser: deps.security.authc.getCurrentUser, + getSpaceId: deps.spaces?.spacesService.getSpaceId, + }; + + this.event$.subscribe(({ message, ...other }) => this.logger.debug(message, other)); + + core.auditTrail.register({ + asScoped: (request: KibanaRequest) => { + return new AuditTrailClient(request, this.event$, depsApi); + }, + }); + + core.logging.configure( + this.config$.pipe( + map((config) => ({ + appenders: { + auditTrailAppender: this.getAppender(config), + }, + loggers: [ + { + // plugins.auditTrail prepended automatically + context: '', + // do not pipe in root log if disabled + level: config.logger.enabled ? 'debug' : 'off', + appenders: ['auditTrailAppender'], + }, + ], + })) + ) + ); + } + + private getAppender(config: AuditTrailConfigType): AppenderConfigType { + return ( + config.appender ?? { + kind: 'console', + layout: { + kind: 'pattern', + highlight: true, + }, + } + ); + } + + public start(core: CoreStart, deps: DepStart) {} + public stop() { + this.event$.complete(); + } +} diff --git a/x-pack/plugins/audit_trail/server/types.ts b/x-pack/plugins/audit_trail/server/types.ts new file mode 100644 index 0000000000000..d0eb0e7eaa981 --- /dev/null +++ b/x-pack/plugins/audit_trail/server/types.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +/** + * Event enhanced with request context data. Provided to an external consumer. + * @public + */ +export interface AuditEvent { + message: string; + type: string; + scope?: string; + user?: string; + space?: string; +} diff --git a/x-pack/plugins/spaces/server/mocks.ts b/x-pack/plugins/spaces/server/mocks.ts new file mode 100644 index 0000000000000..99d547a92eeb6 --- /dev/null +++ b/x-pack/plugins/spaces/server/mocks.ts @@ -0,0 +1,14 @@ +/* + * 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 { spacesServiceMock } from './spaces_service/spaces_service.mock'; + +function createSetupMock() { + return { spacesService: spacesServiceMock.createSetupContract() }; +} + +export const spacesMock = { + createSetup: createSetupMock, +}; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index d466137c40d48..a766e22a34a1d 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -28,6 +28,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { // list paths to the files that contain your plugins tests testFiles: [ + resolve(__dirname, './test_suites/audit_trail'), resolve(__dirname, './test_suites/resolver'), resolve(__dirname, './test_suites/global_search'), ], @@ -50,6 +51,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { )}`, // Required to load new platform plugins via `--plugin-path` flag. '--env.name=development', + + '--xpack.audit_trail.enabled=true', + '--xpack.audit_trail.logger.enabled=true', + '--xpack.audit_trail.appender.kind=file', + '--xpack.audit_trail.appender.path=x-pack/test/plugin_functional/plugins/audit_trail_test/server/pattern_debug.log', + '--xpack.audit_trail.appender.layout.kind=json', ], }, uiSettings: xpackFunctionalConfig.get('uiSettings'), diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json b/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json new file mode 100644 index 0000000000000..f53aa57ad6705 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "audit_trail_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": [], + "requiredPlugins": ["auditTrail"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore new file mode 100644 index 0000000000000..9a3d281179193 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore @@ -0,0 +1 @@ +/*debug.log diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts new file mode 100644 index 0000000000000..dd8d975e7dc66 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { AuditTrailTestPlugin } from './plugin'; + +export const plugin = () => new AuditTrailTestPlugin(); diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts new file mode 100644 index 0000000000000..264f436fb1dc0 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts @@ -0,0 +1,65 @@ +/* + * 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 { Plugin, CoreSetup } from 'src/core/server'; + +export class AuditTrailTestPlugin implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType({ + name: 'audit_trail_test', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: {}, + }, + }); + + const router = core.http.createRouter(); + router.get( + { path: '/audit_trail_test/context/as_current_user', validate: false }, + async (context, request, response) => { + context.core.auditor.withAuditScope('audit_trail_test/context/as_current_user'); + await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); + return response.noContent(); + } + ); + + router.get( + { path: '/audit_trail_test/context/as_internal_user', validate: false }, + async (context, request, response) => { + context.core.auditor.withAuditScope('audit_trail_test/context/as_internal_user'); + await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); + return response.noContent(); + } + ); + + router.get( + { path: '/audit_trail_test/contract/as_current_user', validate: false }, + async (context, request, response) => { + const [coreStart] = await core.getStartServices(); + const auditor = coreStart.auditTrail.asScoped(request); + auditor.withAuditScope('audit_trail_test/contract/as_current_user'); + + await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); + return response.noContent(); + } + ); + + router.get( + { path: '/audit_trail_test/contract/as_internal_user', validate: false }, + async (context, request, response) => { + const [coreStart] = await core.getStartServices(); + const auditor = coreStart.auditTrail.asScoped(request); + auditor.withAuditScope('audit_trail_test/contract/as_internal_user'); + + await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); + return response.noContent(); + } + ); + } + + public start() {} +} diff --git a/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts b/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts new file mode 100644 index 0000000000000..fb66f0dffc12a --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts @@ -0,0 +1,129 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +class FileWrapper { + constructor(private readonly path: string) {} + async reset() { + // "touch" each file to ensure it exists and is empty before each test + await Fs.promises.writeFile(this.path, ''); + } + async read() { + const content = await Fs.promises.readFile(this.path, { encoding: 'utf8' }); + return content.trim().split('\n'); + } + async readJSON() { + const content = await this.read(); + return content.map((l) => JSON.parse(l)); + } + // writing in a file is an async operation. we use this method to make sure logs have been written. + async isNotEmpty() { + const content = await this.read(); + const line = content[0]; + return line.length > 0; + } +} + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('Audit trail service', function () { + this.tags('ciGroup7'); + const logFilePath = Path.resolve( + __dirname, + '../../plugins/audit_trail_test/server/pattern_debug.log' + ); + const logFile = new FileWrapper(logFilePath); + + beforeEach(async () => { + await logFile.reset(); + }); + + it('logs current user access to elasticsearch via RequestHandlerContext', async () => { + await supertest + .get('/audit_trail_test/context/as_current_user') + .set('kbn-xsrf', 'foo') + .expect(204); + + await retry.waitFor('logs event in the dest file', async () => { + return await logFile.isNotEmpty(); + }); + + const content = await logFile.readJSON(); + const pingCall = content.find( + (c) => c.meta.scope === 'audit_trail_test/context/as_current_user' + ); + expect(pingCall).to.be.ok(); + expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); + expect(pingCall.meta.user).to.be('elastic'); + expect(pingCall.meta.space).to.be('default'); + }); + + it('logs internal user access to elasticsearch via RequestHandlerContext', async () => { + await supertest + .get('/audit_trail_test/context/as_internal_user') + .set('kbn-xsrf', 'foo') + .expect(204); + + await retry.waitFor('logs event in the dest file', async () => { + return await logFile.isNotEmpty(); + }); + + const content = await logFile.readJSON(); + const pingCall = content.find( + (c) => c.meta.scope === 'audit_trail_test/context/as_internal_user' + ); + expect(pingCall).to.be.ok(); + expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); + expect(pingCall.meta.user).to.be('elastic'); + expect(pingCall.meta.space).to.be('default'); + }); + + it('logs current user access to elasticsearch via coreStart contract', async () => { + await supertest + .get('/audit_trail_test/contract/as_current_user') + .set('kbn-xsrf', 'foo') + .expect(204); + + await retry.waitFor('logs event in the dest file', async () => { + return await logFile.isNotEmpty(); + }); + + const content = await logFile.readJSON(); + const pingCall = content.find( + (c) => c.meta.scope === 'audit_trail_test/contract/as_current_user' + ); + expect(pingCall).to.be.ok(); + expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); + expect(pingCall.meta.user).to.be('elastic'); + expect(pingCall.meta.space).to.be('default'); + }); + + it('logs internal user access to elasticsearch via coreStart contract', async () => { + await supertest + .get('/audit_trail_test/contract/as_internal_user') + .set('kbn-xsrf', 'foo') + .expect(204); + + await retry.waitFor('logs event in the dest file', async () => { + return await logFile.isNotEmpty(); + }); + + const content = await logFile.readJSON(); + const pingCall = content.find( + (c) => c.meta.scope === 'audit_trail_test/contract/as_internal_user' + ); + expect(pingCall).to.be.ok(); + expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); + expect(pingCall.meta.user).to.be('elastic'); + expect(pingCall.meta.space).to.be('default'); + }); + }); +}