mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
ECS audit logging (#74640)
* ECS audit logging * Apply suggestions from code review Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * Update x-pack/plugins/security/server/authentication/audit_events.ts Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * Update docs/settings/security-settings.asciidoc Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * remove audit trail service from core * fix test * Updated docs and added beta warning * Added dev docs * Tweaks * Plugin list changes * Apply suggestions from technical writers Co-authored-by: Kaarina Tungseth <kaarina.tungseth@elastic.co> * Added docs suggestion * Added api integration tests * Added suggestions from platform team * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update docs/user/security/audit-logging.asciidoc Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update docs/settings/security-settings.asciidoc Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/config.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Added suggestions from PR * Grouped events table * Update x-pack/plugins/security/server/audit/audit_events.ts Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * Update x-pack/plugins/security/server/audit/audit_events.ts Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * Fixed ECS version number in docs Co-authored-by: Larry Gregory <larry.gregory@elastic.co> * Added suggestions from code review * Removed beta * Added suggestions from code review Co-authored-by: Larry Gregory <larry.gregory@elastic.co> Co-authored-by: Kaarina Tungseth <kaarina.tungseth@elastic.co> Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
parent
403c4dac5e
commit
bc8a1dac99
93 changed files with 2466 additions and 2039 deletions
|
@ -286,10 +286,6 @@ which will load the visualization's editor.
|
|||
|To access an elasticsearch instance that has live data you have two options:
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail]
|
||||
|WARNING: Missing README.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement]
|
||||
|Notes:
|
||||
Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place
|
||||
|
@ -469,7 +465,8 @@ Elastic.
|
|||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security]
|
||||
|See Configuring security in Kibana.
|
||||
|See Configuring security in
|
||||
Kibana.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution]
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md)
|
||||
|
||||
## AuditableEvent interface
|
||||
|
||||
Event to audit.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AuditableEvent
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
Not a complete interface.
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [message](./kibana-plugin-core-server.auditableevent.message.md) | <code>string</code> | |
|
||||
| [type](./kibana-plugin-core-server.auditableevent.type.md) | <code>string</code> | |
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
message: string;
|
||||
```
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: string;
|
||||
```
|
|
@ -1,36 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
add(event: AuditableEvent): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| event | <code>AuditableEvent</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`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' });
|
||||
|
||||
```
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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. |
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
withAuditScope(name: string): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| name | <code>string</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
asScoped(request: KibanaRequest): Auditor;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| request | <code>KibanaRequest</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Auditor`
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AuditorFactory
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [asScoped(request)](./kibana-plugin-core-server.auditorfactory.asscoped.md) | |
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md)
|
||||
|
||||
## AuditTrailSetup interface
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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. |
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
register(auditor: AuditorFactory): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| auditor | <code>AuditorFactory</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md)
|
||||
|
||||
## AuditTrailStart type
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AuditTrailStart = AuditorFactory;
|
||||
```
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
auditTrail: AuditTrailSetup;
|
||||
```
|
|
@ -16,7 +16,6 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [auditTrail](./kibana-plugin-core-server.coresetup.audittrail.md) | <code>AuditTrailSetup</code> | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) |
|
||||
| [capabilities](./kibana-plugin-core-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) |
|
||||
| [context](./kibana-plugin-core-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) |
|
||||
| [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) |
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
auditTrail: AuditTrailStart;
|
||||
```
|
|
@ -16,7 +16,6 @@ export interface CoreStart
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) | <code>AuditTrailStart</code> | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) |
|
||||
| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | <code>CapabilitiesStart</code> | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) |
|
||||
| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | <code>ElasticsearchServiceStart</code> | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) |
|
||||
| [http](./kibana-plugin-core-server.corestart.http.md) | <code>HttpServiceStart</code> | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) |
|
||||
|
|
|
@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyClusterClient` class
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders);
|
||||
constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -18,6 +18,5 @@ constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFact
|
|||
| --- | --- | --- |
|
||||
| config | <code>LegacyElasticsearchClientConfig</code> | |
|
||||
| log | <code>Logger</code> | |
|
||||
| getAuditorFactory | <code>() => AuditorFactory</code> | |
|
||||
| getAuthHeaders | <code>GetAuthHeaders</code> | |
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export declare class LegacyClusterClient implements ILegacyClusterClient
|
|||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(config, log, getAuditorFactory, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyClusterClient</code> class |
|
||||
| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyClusterClient</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyScopedClusterClient` class
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined);
|
||||
constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -19,5 +19,4 @@ constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller
|
|||
| internalAPICaller | <code>LegacyAPICaller</code> | |
|
||||
| scopedAPICaller | <code>LegacyAPICaller</code> | |
|
||||
| headers | <code>Headers | undefined</code> | |
|
||||
| auditor | <code>Auditor | undefined</code> | |
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export declare class LegacyScopedClusterClient implements ILegacyScopedClusterCl
|
|||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(internalAPICaller, scopedAPICaller, headers, auditor)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyScopedClusterClient</code> class |
|
||||
| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the <code>LegacyScopedClusterClient</code> class |
|
||||
|
||||
## Methods
|
||||
|
||||
|
|
|
@ -53,10 +53,6 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav |
|
||||
| [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) | |
|
||||
|
@ -132,7 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. |
|
||||
| [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
|
||||
| [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
|
||||
| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request |
|
||||
| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
|
||||
| [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. |
|
||||
| [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. |
|
||||
| [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route |
|
||||
|
@ -223,7 +219,6 @@ 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) | |
|
||||
|
|
|
@ -21,6 +21,5 @@ core: {
|
|||
uiSettings: {
|
||||
client: IUiSettingsClient;
|
||||
};
|
||||
auditor: Auditor;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
Plugin specific context passed to a route handler.
|
||||
|
||||
Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request
|
||||
Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
@ -18,5 +18,5 @@ export interface RequestHandlerContext
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> client: IScopedClusterClient;</code><br/><code> legacy: {</code><br/><code> client: ILegacyScopedClusterClient;</code><br/><code> };</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> auditor: Auditor;</code><br/><code> }</code> | |
|
||||
| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> client: IScopedClusterClient;</code><br/><code> legacy: {</code><br/><code> client: ILegacyScopedClusterClient;</code><br/><code> };</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ There is a very limited set of cases when you'd want to change these settings. F
|
|||
| `xpack.security.authc.http.autoSchemesEnabled`
|
||||
| Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`.
|
||||
|
||||
| `xpack.security.authc.http.schemes`
|
||||
| `xpack.security.authc.http.schemes[]`
|
||||
| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <<api-keys, `ApiKey`>> scheme.
|
||||
|
||||
|===
|
||||
|
@ -240,7 +240,6 @@ The format is a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h',
|
|||
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[security-encrypted-saved-objects-settings]]
|
||||
==== Encrypted saved objects settings
|
||||
|
||||
|
@ -261,4 +260,112 @@ In high-availability deployments, make sure you use the same encryption and decr
|
|||
`keyRotation.decryptionOnlyKeys`
|
||||
| An optional list of previously used encryption keys. Like <<xpack-encryptedSavedObjects-encryptionKey, `xpack.encryptedSavedObjects.encryptionKey`>>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key.
|
||||
|
||||
|===
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[audit-logging-settings]]
|
||||
===== Audit logging settings
|
||||
|
||||
You can enable audit logging to support compliance, accountability, and security. When enabled, {kib} will capture:
|
||||
|
||||
- Who performed an action
|
||||
- What action was performed
|
||||
- When the action occurred
|
||||
|
||||
For more details and a reference of audit events, refer to <<xpack-security-audit-logging>>.
|
||||
|
||||
[cols="2*<"]
|
||||
|===
|
||||
| `xpack.security.audit.enabled`
|
||||
| Set to `true` to enable audit logging for security events. *Default:* `false`
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[ecs-audit-logging-settings]]
|
||||
===== ECS audit logging settings
|
||||
|
||||
To enable the <<xpack-security-ecs-audit-logging, ECS audit logger>>, specify where you want to write the audit events using `xpack.security.audit.appender`.
|
||||
|
||||
[cols="2*<"]
|
||||
|===
|
||||
| `xpack.security.audit.appender`
|
||||
| Optional. Specifies where audit logs should be written to and how they should be formatted.
|
||||
|
||||
2+a| For example:
|
||||
|
||||
[source,yaml]
|
||||
----------------------------------------
|
||||
xpack.security.audit.appender:
|
||||
kind: file
|
||||
path: /path/to/audit.log
|
||||
layout:
|
||||
kind: json
|
||||
----------------------------------------
|
||||
|
||||
| `xpack.security.audit.appender.kind`
|
||||
| Required. Specifies where audit logs should be written to. Allowed values are `console` or `file`.
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[audit-logging-file-appender]]
|
||||
===== File appender
|
||||
|
||||
The file appender can be configured using the following settings:
|
||||
|
||||
[cols="2*<"]
|
||||
|===
|
||||
| `xpack.security.audit.appender.path`
|
||||
| Required. Full file path the log file should be written to.
|
||||
|
||||
| `xpack.security.audit.appender.layout.kind`
|
||||
| Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`.
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[audit-logging-pattern-layout]]
|
||||
===== Pattern layout
|
||||
|
||||
The pattern layout can be configured using the following settings:
|
||||
|
||||
[cols="2*<"]
|
||||
|===
|
||||
| `xpack.security.audit.appender.layout.highlight`
|
||||
| Optional. Set to `true` to enable highlighting log messages with colors.
|
||||
|
||||
| `xpack.security.audit.appender.layout.pattern`
|
||||
| Optional. Specifies how the log line should be formatted. *Default:* `[%date][%level][%logger]%meta %message`
|
||||
|===
|
||||
|
||||
[float]
|
||||
[[audit-logging-ignore-filters]]
|
||||
===== Ignore filters
|
||||
|
||||
[cols="2*<"]
|
||||
|===
|
||||
| `xpack.security.audit.ignore_filters[]`
|
||||
| List of filters that determine which events should be excluded from the audit log. An event will get filtered out if at least one of the provided filters matches.
|
||||
|
||||
2+a| For example:
|
||||
|
||||
[source,yaml]
|
||||
----------------------------------------
|
||||
xpack.security.audit.ignore_filters:
|
||||
- actions: [http_request] <1>
|
||||
- categories: [database]
|
||||
types: [creation, change, deletion] <2>
|
||||
----------------------------------------
|
||||
<1> Filters out HTTP request events
|
||||
<2> Filters out any data write events
|
||||
|
||||
| `xpack.security.audit.ignore_filters[].actions[]`
|
||||
| List of values matched against the `event.action` field of an audit event. Refer to <<xpack-security-audit-logging>> for a list of available events.
|
||||
|
||||
| `xpack.security.audit.ignore_filters[].categories[]`
|
||||
| List of values matched against the `event.category` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-category.html[ECS categorization field] for allowed values.
|
||||
|
||||
| `xpack.security.audit.ignore_filters[].types[]`
|
||||
| List of values matched against the `event.type` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-type.html[ECS type field] for allowed values.
|
||||
|
||||
| `xpack.security.audit.ignore_filters[].outcomes[]`
|
||||
| List of values matched against the `event.outcome` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-outcome.html[ECS outcome field] for allowed values.
|
||||
|===
|
||||
|
|
|
@ -3,30 +3,30 @@
|
|||
=== Audit logs
|
||||
|
||||
You can enable auditing to keep track of security-related events such as
|
||||
authorization success and failures. Logging these events enables you
|
||||
to monitor {kib} for suspicious activity and provides evidence in the
|
||||
event of an attack.
|
||||
authorization success and failures. Logging these events enables you to monitor
|
||||
{kib} for suspicious activity and provides evidence in the event of an attack.
|
||||
|
||||
Use the {kib} audit logs in conjunction with {es}'s
|
||||
audit logging to get a holistic view of all security related events.
|
||||
{kib} defers to {es}'s security model for authentication, data
|
||||
index authorization, and features that are driven by cluster-wide privileges.
|
||||
For more information on enabling audit logging in {es}, see
|
||||
{ref}/auditing.html[Auditing security events].
|
||||
Use the {kib} audit logs in conjunction with {ref}/enable-audit-logging.html[{es} audit logging] to get a
|
||||
holistic view of all security related events. {kib} defers to the {es} security
|
||||
model for authentication, data index authorization, and features that are driven
|
||||
by cluster-wide privileges. For more information on enabling audit logging in
|
||||
{es}, refer to {ref}/auditing.html[Auditing security events].
|
||||
|
||||
[IMPORTANT]
|
||||
============================================================================
|
||||
Audit logs are **disabled** by default. To enable this functionality, you
|
||||
must set `xpack.security.audit.enabled` to `true` in `kibana.yml`.
|
||||
Audit logs are **disabled** by default. To enable this functionality, you must
|
||||
set `xpack.security.audit.enabled` to `true` in `kibana.yml`.
|
||||
============================================================================
|
||||
|
||||
Audit logging uses the standard {kib} logging output, which can be configured
|
||||
in the `kibana.yml` and is discussed in <<settings>>.
|
||||
The current version of the audit logger uses the standard {kib} logging output,
|
||||
which can be configured in `kibana.yml`. For more information, refer to <<settings>>.
|
||||
The audit logger uses a separate logger and can be configured using
|
||||
the options in <<audit-logging-settings>>.
|
||||
|
||||
==== Audit event types
|
||||
|
||||
When you are auditing security events, each request can generate
|
||||
multiple audit events. The following is a list of the events that can be generated:
|
||||
When you are auditing security events, each request can generate multiple audit
|
||||
events. The following is a list of the events that can be generated:
|
||||
|
||||
|======
|
||||
| `saved_objects_authorization_success` | Logged when a user is authorized to access a saved
|
||||
|
@ -34,3 +34,110 @@ multiple audit events. The following is a list of the events that can be generat
|
|||
| `saved_objects_authorization_failure` | Logged when a user isn't authorized to access a saved
|
||||
objects when using a role with <<kibana-privileges>>
|
||||
|======
|
||||
|
||||
[[xpack-security-ecs-audit-logging]]
|
||||
==== ECS audit events
|
||||
|
||||
[IMPORTANT]
|
||||
============================================================================
|
||||
The following events are only logged if the ECS audit logger is enabled.
|
||||
For information on how to configure `xpack.security.audit.appender`, refer to
|
||||
<<ecs-audit-logging-settings>>.
|
||||
============================================================================
|
||||
|
||||
Refer to the table of events that can be logged for auditing purposes.
|
||||
|
||||
Each event is broken down into `category`, `type`, `action` and `outcome` fields
|
||||
to make it easy to filter, query and aggregate the resulting logs.
|
||||
|
||||
[NOTE]
|
||||
============================================================================
|
||||
To ensure that a record of every operation is persisted even in case of an
|
||||
unexpected error, asynchronous write operations are logged immediately after all
|
||||
authorization checks have passed, but before the response from {es} is received.
|
||||
Refer to the corresponding {es} logs for potential write errors.
|
||||
============================================================================
|
||||
|
||||
|
||||
[cols="3*<"]
|
||||
|======
|
||||
3+a|
|
||||
===== Category: authentication
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
.2+| `user_login`
|
||||
| `success` | User has logged in successfully.
|
||||
| `failure` | Failed login attempt (e.g. due to invalid credentials).
|
||||
|
||||
3+a|
|
||||
===== Category: database
|
||||
====== Type: creation
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
.2+| `saved_object_create`
|
||||
| `unknown` | User is creating a saved object.
|
||||
| `failure` | User is not authorized to create a saved object.
|
||||
|
||||
|
||||
3+a|
|
||||
====== Type: change
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
.2+| `saved_object_update`
|
||||
| `unknown` | User is updating a saved object.
|
||||
| `failure` | User is not authorized to update a saved object.
|
||||
|
||||
.2+| `saved_object_add_to_spaces`
|
||||
| `unknown` | User is adding a saved object to other spaces.
|
||||
| `failure` | User is not authorized to add a saved object to other spaces.
|
||||
|
||||
.2+| `saved_object_delete_from_spaces`
|
||||
| `unknown` | User is removing a saved object from other spaces.
|
||||
| `failure` | User is not authorized to remove a saved object from other spaces.
|
||||
|
||||
3+a|
|
||||
====== Type: deletion
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
.2+| `saved_object_delete`
|
||||
| `unknown` | User is deleting a saved object.
|
||||
| `failure` | User is not authorized to delete a saved object.
|
||||
|
||||
3+a|
|
||||
====== Type: access
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
.2+| `saved_object_get`
|
||||
| `success` | User has accessed a saved object.
|
||||
| `failure` | User is not authorized to access a saved object.
|
||||
|
||||
.2+| `saved_object_find`
|
||||
| `success` | User has accessed a saved object as part of a search operation.
|
||||
| `failure` | User is not authorized to search for saved objects.
|
||||
|
||||
|
||||
3+a|
|
||||
===== Category: web
|
||||
|
||||
| *Action*
|
||||
| *Outcome*
|
||||
| *Description*
|
||||
|
||||
| `http_request`
|
||||
| `unknown` | User is making an HTTP request.
|
||||
|======
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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 type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { AuditTrailSetup, AuditTrailStart, Auditor } from './types';
|
||||
import { AuditTrailService } from './audit_trail_service';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const mocked: jest.Mocked<AuditTrailSetup> = {
|
||||
register: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
const createAuditorMock = () => {
|
||||
const mocked: jest.Mocked<Auditor> = {
|
||||
add: jest.fn(),
|
||||
withAuditScope: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const mocked: jest.Mocked<AuditTrailStart> = {
|
||||
asScoped: jest.fn(),
|
||||
};
|
||||
mocked.asScoped.mockReturnValue(createAuditorMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
const createServiceMock = (): jest.Mocked<PublicMethodsOf<AuditTrailService>> => ({
|
||||
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,
|
||||
};
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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<AuditTrailSetup, AuditTrailStart> {
|
||||
private readonly log: Logger;
|
||||
private auditor: AuditorFactory = defaultAuditorFactory;
|
||||
private readonly auditors = new WeakMap<LegacyRequest, Auditor>();
|
||||
|
||||
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() {}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* 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';
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* 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;
|
|
@ -19,41 +19,6 @@
|
|||
import { CoreRouteHandlerContext } from './core_route_handler_context';
|
||||
import { coreMock, httpServerMock } from './mocks';
|
||||
|
||||
describe('#auditor', () => {
|
||||
test('returns the results of coreStart.audiTrail.asScoped', () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const coreStart = coreMock.createInternalStart();
|
||||
const context = new CoreRouteHandlerContext(coreStart, request);
|
||||
|
||||
const auditor = context.auditor;
|
||||
expect(auditor).toBe(coreStart.auditTrail.asScoped.mock.results[0].value);
|
||||
});
|
||||
|
||||
test('lazily created', () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const coreStart = coreMock.createInternalStart();
|
||||
const context = new CoreRouteHandlerContext(coreStart, request);
|
||||
|
||||
expect(coreStart.auditTrail.asScoped).not.toHaveBeenCalled();
|
||||
const auditor = context.auditor;
|
||||
expect(coreStart.auditTrail.asScoped).toHaveBeenCalled();
|
||||
expect(auditor).toBeDefined();
|
||||
});
|
||||
|
||||
test('only creates one instance', () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const coreStart = coreMock.createInternalStart();
|
||||
const context = new CoreRouteHandlerContext(coreStart, request);
|
||||
|
||||
const auditor1 = context.auditor;
|
||||
const auditor2 = context.auditor;
|
||||
expect(coreStart.auditTrail.asScoped.mock.calls.length).toBe(1);
|
||||
const mockResult = coreStart.auditTrail.asScoped.mock.results[0].value;
|
||||
expect(auditor1).toBe(mockResult);
|
||||
expect(auditor2).toBe(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#elasticsearch', () => {
|
||||
describe('#client', () => {
|
||||
test('returns the results of coreStart.elasticsearch.client.asScoped', () => {
|
||||
|
|
|
@ -27,7 +27,6 @@ import {
|
|||
IScopedClusterClient,
|
||||
LegacyScopedClusterClient,
|
||||
} from './elasticsearch';
|
||||
import { Auditor } from './audit_trail';
|
||||
import { InternalUiSettingsServiceStart, IUiSettingsClient } from './ui_settings';
|
||||
|
||||
class CoreElasticsearchRouteHandlerContext {
|
||||
|
@ -99,8 +98,6 @@ class CoreUiSettingsRouteHandlerContext {
|
|||
}
|
||||
|
||||
export class CoreRouteHandlerContext {
|
||||
#auditor?: Auditor;
|
||||
|
||||
readonly elasticsearch: CoreElasticsearchRouteHandlerContext;
|
||||
readonly savedObjects: CoreSavedObjectsRouteHandlerContext;
|
||||
readonly uiSettings: CoreUiSettingsRouteHandlerContext;
|
||||
|
@ -122,11 +119,4 @@ export class CoreRouteHandlerContext {
|
|||
this.savedObjects
|
||||
);
|
||||
}
|
||||
|
||||
public get auditor() {
|
||||
if (this.#auditor == null) {
|
||||
this.#auditor = this.coreStart.auditTrail.asScoped(this.request);
|
||||
}
|
||||
return this.#auditor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import { configServiceMock, getEnvOptions } from '../config/mocks';
|
|||
import { CoreContext } from '../core_context';
|
||||
import { loggingSystemMock } from '../logging/logging_system.mock';
|
||||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock';
|
||||
import { ElasticsearchConfig } from './elasticsearch_config';
|
||||
import { ElasticsearchService } from './elasticsearch_service';
|
||||
import { elasticsearchServiceMock } from './elasticsearch_service.mock';
|
||||
|
@ -41,9 +40,6 @@ const configService = configServiceMock.create();
|
|||
const setupDeps = {
|
||||
http: httpServiceMock.createInternalSetupContract(),
|
||||
};
|
||||
const startDeps = {
|
||||
auditTrail: auditTrailServiceMock.createStartContract(),
|
||||
};
|
||||
configService.atPath.mockReturnValue(
|
||||
new BehaviorSubject({
|
||||
hosts: ['http://1.2.3.4'],
|
||||
|
@ -113,7 +109,6 @@ describe('#setup', () => {
|
|||
expect(MockLegacyClusterClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining(customConfig),
|
||||
expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }),
|
||||
expect.any(Function),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
@ -260,14 +255,14 @@ describe('#setup', () => {
|
|||
|
||||
describe('#start', () => {
|
||||
it('throws if called before `setup`', async () => {
|
||||
expect(() => elasticsearchService.start(startDeps)).rejects.toMatchInlineSnapshot(
|
||||
expect(() => elasticsearchService.start()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: ElasticsearchService needs to be setup before calling start]`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns elasticsearch client as a part of the contract', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
const startContract = await elasticsearchService.start(startDeps);
|
||||
const startContract = await elasticsearchService.start();
|
||||
const client = startContract.client;
|
||||
|
||||
expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser);
|
||||
|
@ -276,7 +271,7 @@ describe('#start', () => {
|
|||
describe('#createClient', () => {
|
||||
it('allows to specify config properties', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
const startContract = await elasticsearchService.start(startDeps);
|
||||
const startContract = await elasticsearchService.start();
|
||||
|
||||
// reset all mocks called during setup phase
|
||||
MockClusterClient.mockClear();
|
||||
|
@ -295,7 +290,7 @@ describe('#start', () => {
|
|||
});
|
||||
it('creates a new client on each call', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
const startContract = await elasticsearchService.start(startDeps);
|
||||
const startContract = await elasticsearchService.start();
|
||||
|
||||
// reset all mocks called during setup phase
|
||||
MockClusterClient.mockClear();
|
||||
|
@ -310,7 +305,7 @@ describe('#start', () => {
|
|||
|
||||
it('falls back to elasticsearch default config values if property not specified', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
const startContract = await elasticsearchService.start(startDeps);
|
||||
const startContract = await elasticsearchService.start();
|
||||
|
||||
// reset all mocks called during setup phase
|
||||
MockClusterClient.mockClear();
|
||||
|
@ -347,7 +342,7 @@ describe('#start', () => {
|
|||
describe('#stop', () => {
|
||||
it('stops both legacy and new clients', async () => {
|
||||
await elasticsearchService.setup(setupDeps);
|
||||
await elasticsearchService.start(startDeps);
|
||||
await elasticsearchService.start();
|
||||
await elasticsearchService.stop();
|
||||
|
||||
expect(mockLegacyClusterClientInstance.close).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client';
|
||||
import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config';
|
||||
import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/';
|
||||
import { AuditTrailStart, AuditorFactory } from '../audit_trail';
|
||||
import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types';
|
||||
import { pollEsNodesVersion } from './version_check/ensure_es_version';
|
||||
import { calculateStatus$ } from './status';
|
||||
|
@ -41,16 +40,11 @@ interface SetupDeps {
|
|||
http: InternalHttpServiceSetup;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
auditTrail: AuditTrailStart;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ElasticsearchService
|
||||
implements CoreService<InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart> {
|
||||
private readonly log: Logger;
|
||||
private readonly config$: Observable<ElasticsearchConfig>;
|
||||
private auditorFactory?: AuditorFactory;
|
||||
private stop$ = new Subject();
|
||||
private kibanaVersion: string;
|
||||
private getAuthHeaders?: GetAuthHeaders;
|
||||
|
@ -103,8 +97,7 @@ export class ElasticsearchService
|
|||
status$: calculateStatus$(esNodesCompatibility$),
|
||||
};
|
||||
}
|
||||
public async start({ auditTrail }: StartDeps): Promise<InternalElasticsearchServiceStart> {
|
||||
this.auditorFactory = auditTrail;
|
||||
public async start(): Promise<InternalElasticsearchServiceStart> {
|
||||
if (!this.legacyClient || !this.createLegacyCustomClient) {
|
||||
throw new Error('ElasticsearchService needs to be setup before calling start');
|
||||
}
|
||||
|
@ -153,15 +146,7 @@ export class ElasticsearchService
|
|||
return new LegacyClusterClient(
|
||||
config,
|
||||
this.coreContext.logger.get('elasticsearch', type),
|
||||
this.getAuditorFactory,
|
||||
this.getAuthHeaders
|
||||
);
|
||||
}
|
||||
|
||||
private getAuditorFactory = () => {
|
||||
if (!this.auditorFactory) {
|
||||
throw new Error('auditTrail has not been initialized');
|
||||
}
|
||||
return this.auditorFactory;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ 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';
|
||||
|
@ -43,11 +42,7 @@ test('#constructor creates client with parsed config', () => {
|
|||
const mockEsConfig = { apiVersion: 'es-version' } as any;
|
||||
const mockLogger = logger.get();
|
||||
|
||||
const clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
expect(clusterClient).toBeDefined();
|
||||
|
||||
expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1);
|
||||
|
@ -73,11 +68,7 @@ describe('#callAsInternalUser', () => {
|
|||
};
|
||||
MockClient.mockImplementation(() => mockEsClientInstance);
|
||||
|
||||
clusterClient = new LegacyClusterClient(
|
||||
{ apiVersion: 'es-version' } as any,
|
||||
logger.get(),
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient({ apiVersion: 'es-version' } as any, logger.get());
|
||||
});
|
||||
|
||||
test('fails if cluster client is closed', async () => {
|
||||
|
@ -246,11 +237,7 @@ describe('#asScoped', () => {
|
|||
requestHeadersWhitelist: ['one', 'two'],
|
||||
} as any;
|
||||
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
@ -285,11 +272,7 @@ describe('#asScoped', () => {
|
|||
|
||||
test('properly configures `ignoreCertAndKey` for various configurations', () => {
|
||||
// Config without SSL.
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
|
||||
mockParseElasticsearchClientConfig.mockClear();
|
||||
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
|
||||
|
@ -302,11 +285,7 @@ describe('#asScoped', () => {
|
|||
|
||||
// Config ssl.alwaysPresentCertificate === false
|
||||
mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any;
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
|
||||
mockParseElasticsearchClientConfig.mockClear();
|
||||
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
|
||||
|
@ -319,11 +298,7 @@ describe('#asScoped', () => {
|
|||
|
||||
// Config ssl.alwaysPresentCertificate === true
|
||||
mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any;
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
|
||||
mockParseElasticsearchClientConfig.mockClear();
|
||||
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
|
||||
|
@ -344,8 +319,7 @@ describe('#asScoped', () => {
|
|||
expect(MockScopedClusterClient).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
{ one: '1', two: '2' },
|
||||
expect.any(Object)
|
||||
{ one: '1', two: '2' }
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -360,8 +334,7 @@ describe('#asScoped', () => {
|
|||
expect(MockScopedClusterClient).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
{ 'x-opaque-id': 'alpha' },
|
||||
expect.any(Object)
|
||||
{ 'x-opaque-id': 'alpha' }
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -383,142 +356,75 @@ describe('#asScoped', () => {
|
|||
});
|
||||
|
||||
test('does not fail when scope to not defined request', async () => {
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
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,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
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,
|
||||
auditTrailServiceMock.createAuditorFactory,
|
||||
() => ({
|
||||
one: '1',
|
||||
three: '3',
|
||||
})
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({
|
||||
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' },
|
||||
expect.any(Object)
|
||||
{ one: '1', two: '2' }
|
||||
);
|
||||
});
|
||||
|
||||
test('getAuthHeaders results rewrite extends a request headers', async () => {
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory,
|
||||
() => ({ one: 'foo' })
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ 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' },
|
||||
expect.any(Object)
|
||||
{ one: 'foo', two: '2' }
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't call getAuthHeaders for a fake request", async () => {
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory,
|
||||
() => ({})
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({}));
|
||||
clusterClient.asScoped({ headers: { one: 'foo' } });
|
||||
|
||||
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
|
||||
expect(MockScopedClusterClient).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
{ one: 'foo' },
|
||||
undefined
|
||||
{ one: 'foo' }
|
||||
);
|
||||
});
|
||||
|
||||
test('filters a fake request headers', async () => {
|
||||
clusterClient = new LegacyClusterClient(
|
||||
mockEsConfig,
|
||||
mockLogger,
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
);
|
||||
clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger);
|
||||
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' },
|
||||
undefined
|
||||
{ one: '1', two: '2' }
|
||||
);
|
||||
});
|
||||
|
||||
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),
|
||||
expect.objectContaining({ 'x-opaque-id': expect.any(String) }),
|
||||
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', () => {
|
||||
|
@ -536,8 +442,7 @@ describe('#close', () => {
|
|||
|
||||
clusterClient = new LegacyClusterClient(
|
||||
{ apiVersion: 'es-version', requestHeadersWhitelist: [] } as any,
|
||||
logger.get(),
|
||||
auditTrailServiceMock.createAuditorFactory
|
||||
logger.get()
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -20,8 +20,7 @@ import { Client } from 'elasticsearch';
|
|||
import { get } from 'lodash';
|
||||
|
||||
import { LegacyElasticsearchErrorHelpers } from './errors';
|
||||
import { GetAuthHeaders, KibanaRequest, isKibanaRequest, isRealRequest } from '../../http';
|
||||
import { AuditorFactory } from '../../audit_trail';
|
||||
import { GetAuthHeaders, isKibanaRequest, isRealRequest } from '../../http';
|
||||
import { filterHeaders, ensureRawRequest } from '../../http/router';
|
||||
import { Logger } from '../../logging';
|
||||
import { ScopeableRequest } from '../types';
|
||||
|
@ -132,7 +131,6 @@ 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));
|
||||
|
@ -210,20 +208,10 @@ export class LegacyClusterClient implements ILegacyClusterClient {
|
|||
filterHeaders(this.getHeaders(request), [
|
||||
'x-opaque-id',
|
||||
...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 = isKibanaRequest(request) ? 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).
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import { LegacyScopedClusterClient } from './scoped_cluster_client';
|
||||
import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock';
|
||||
|
||||
let internalAPICaller: jest.Mock;
|
||||
let scopedAPICaller: jest.Mock;
|
||||
|
@ -84,28 +83,6 @@ 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', () => {
|
||||
|
@ -229,26 +206,4 @@ 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import { intersection, isObject } from 'lodash';
|
||||
import { Auditor } from '../../audit_trail';
|
||||
import { Headers } from '../../http/router';
|
||||
import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types';
|
||||
|
||||
|
@ -47,8 +46,7 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient {
|
|||
constructor(
|
||||
private readonly internalAPICaller: LegacyAPICaller,
|
||||
private readonly scopedAPICaller: LegacyAPICaller,
|
||||
private readonly headers?: Headers,
|
||||
private readonly auditor?: Auditor
|
||||
private readonly headers?: Headers
|
||||
) {
|
||||
this.callAsCurrentUser = this.callAsCurrentUser.bind(this);
|
||||
this.callAsInternalUser = this.callAsInternalUser.bind(this);
|
||||
|
@ -68,13 +66,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient {
|
|||
clientParams: Record<string, any> = {},
|
||||
options?: LegacyCallAPIOptions
|
||||
) {
|
||||
if (this.auditor) {
|
||||
this.auditor.add({
|
||||
message: endpoint,
|
||||
type: 'elasticsearch.call.internalUser',
|
||||
});
|
||||
}
|
||||
|
||||
return this.internalAPICaller(endpoint, clientParams, options);
|
||||
}
|
||||
|
||||
|
@ -107,13 +98,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,6 @@ import {
|
|||
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
|
||||
import { MetricsServiceSetup, MetricsServiceStart } from './metrics';
|
||||
import { StatusServiceSetup } from './status';
|
||||
import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail';
|
||||
import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging';
|
||||
import { CoreUsageDataStart } from './core_usage_data';
|
||||
|
||||
|
@ -77,7 +76,6 @@ import {
|
|||
|
||||
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
|
||||
|
||||
export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail';
|
||||
export { bootstrap } from './bootstrap';
|
||||
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
|
||||
export {
|
||||
|
@ -378,7 +376,6 @@ export { CoreUsageDataStart } from './core_usage_data';
|
|||
* data client which uses the credentials of the incoming request
|
||||
* - {@link IUiSettingsClient | uiSettings.client} - uiSettings client
|
||||
* which uses the credentials of the incoming request
|
||||
* - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
@ -397,7 +394,6 @@ export interface RequestHandlerContext {
|
|||
uiSettings: {
|
||||
client: IUiSettingsClient;
|
||||
};
|
||||
auditor: Auditor;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -434,8 +430,6 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
uiSettings: UiSettingsServiceSetup;
|
||||
/** {@link StartServicesAccessor} */
|
||||
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
|
||||
/** {@link AuditTrailSetup} */
|
||||
auditTrail: AuditTrailSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -469,8 +463,6 @@ export interface CoreStart {
|
|||
savedObjects: SavedObjectsServiceStart;
|
||||
/** {@link UiSettingsServiceStart} */
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
/** {@link AuditTrailSetup} */
|
||||
auditTrail: AuditTrailStart;
|
||||
/** @internal {@link CoreUsageDataStart} */
|
||||
coreUsageData: CoreUsageDataStart;
|
||||
}
|
||||
|
@ -483,7 +475,6 @@ export {
|
|||
PluginsServiceSetup,
|
||||
PluginsServiceStart,
|
||||
PluginOpaqueId,
|
||||
AuditTrailStart,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,6 @@ import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metr
|
|||
import { InternalRenderingServiceSetup } from './rendering';
|
||||
import { InternalHttpResourcesSetup } from './http_resources';
|
||||
import { InternalStatusServiceSetup } from './status';
|
||||
import { AuditTrailSetup, AuditTrailStart } from './audit_trail';
|
||||
import { InternalLoggingServiceSetup } from './logging';
|
||||
import { CoreUsageDataStart } from './core_usage_data';
|
||||
|
||||
|
@ -53,7 +52,6 @@ export interface InternalCoreSetup {
|
|||
environment: InternalEnvironmentServiceSetup;
|
||||
rendering: InternalRenderingServiceSetup;
|
||||
httpResources: InternalHttpResourcesSetup;
|
||||
auditTrail: AuditTrailSetup;
|
||||
logging: InternalLoggingServiceSetup;
|
||||
metrics: InternalMetricsServiceSetup;
|
||||
}
|
||||
|
@ -68,7 +66,6 @@ export interface InternalCoreStart {
|
|||
metrics: InternalMetricsServiceStart;
|
||||
savedObjects: InternalSavedObjectsServiceStart;
|
||||
uiSettings: InternalUiSettingsServiceStart;
|
||||
auditTrail: AuditTrailStart;
|
||||
coreUsageData: CoreUsageDataStart;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types';
|
|||
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';
|
||||
import { metricsServiceMock } from '../metrics/metrics_service.mock';
|
||||
|
||||
|
@ -92,7 +91,6 @@ beforeEach(() => {
|
|||
rendering: renderingServiceMock,
|
||||
environment: environmentSetup,
|
||||
status: statusServiceMock.createInternalSetupContract(),
|
||||
auditTrail: auditTrailServiceMock.createSetupContract(),
|
||||
logging: loggingServiceMock.createInternalSetupContract(),
|
||||
metrics: metricsServiceMock.createInternalSetupContract(),
|
||||
},
|
||||
|
|
|
@ -216,7 +216,6 @@ export class LegacyService implements CoreService {
|
|||
getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$,
|
||||
},
|
||||
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
|
||||
auditTrail: startDeps.core.auditTrail,
|
||||
coreUsageData: {
|
||||
getCoreUsageData: () => {
|
||||
throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy');
|
||||
|
@ -284,7 +283,6 @@ export class LegacyService implements CoreService {
|
|||
uiSettings: {
|
||||
register: setupDeps.core.uiSettings.register,
|
||||
},
|
||||
auditTrail: setupDeps.core.auditTrail,
|
||||
getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]),
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ import { capabilitiesServiceMock } from './capabilities/capabilities_service.moc
|
|||
import { metricsServiceMock } from './metrics/metrics_service.mock';
|
||||
import { environmentServiceMock } from './environment/environment_service.mock';
|
||||
import { statusServiceMock } from './status/status_service.mock';
|
||||
import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock';
|
||||
import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
|
||||
|
||||
export { configServiceMock } from './config/mocks';
|
||||
|
@ -139,7 +138,6 @@ function createCoreSetupMock({
|
|||
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
|
||||
status: statusServiceMock.createSetupContract(),
|
||||
uiSettings: uiSettingsMock,
|
||||
auditTrail: auditTrailServiceMock.createSetupContract(),
|
||||
logging: loggingServiceMock.createSetupContract(),
|
||||
metrics: metricsServiceMock.createSetupContract(),
|
||||
getStartServices: jest
|
||||
|
@ -152,7 +150,6 @@ function createCoreSetupMock({
|
|||
|
||||
function createCoreStartMock() {
|
||||
const mock: MockedKeys<CoreStart> = {
|
||||
auditTrail: auditTrailServiceMock.createStartContract(),
|
||||
capabilities: capabilitiesServiceMock.createStartContract(),
|
||||
elasticsearch: elasticsearchServiceMock.createStart(),
|
||||
http: httpServiceMock.createStartContract(),
|
||||
|
@ -177,7 +174,6 @@ function createInternalCoreSetupMock() {
|
|||
httpResources: httpResourcesMock.createSetupContract(),
|
||||
rendering: renderingMock.createSetupContract(),
|
||||
uiSettings: uiSettingsServiceMock.createSetupContract(),
|
||||
auditTrail: auditTrailServiceMock.createSetupContract(),
|
||||
logging: loggingServiceMock.createInternalSetupContract(),
|
||||
metrics: metricsServiceMock.createInternalSetupContract(),
|
||||
};
|
||||
|
@ -192,7 +188,6 @@ function createInternalCoreStartMock() {
|
|||
metrics: metricsServiceMock.createInternalStartContract(),
|
||||
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
auditTrail: auditTrailServiceMock.createStartContract(),
|
||||
coreUsageData: coreUsageDataServiceMock.createStartContract(),
|
||||
};
|
||||
return startDeps;
|
||||
|
@ -213,7 +208,6 @@ function createCoreRequestHandlerContextMock() {
|
|||
uiSettings: {
|
||||
client: uiSettingsServiceMock.createClient(),
|
||||
},
|
||||
auditor: auditTrailServiceMock.createAuditor(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -201,7 +201,6 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
register: deps.uiSettings.register,
|
||||
},
|
||||
getStartServices: () => plugin.startDependencies,
|
||||
auditTrail: deps.auditTrail,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -250,7 +249,6 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
|
|||
uiSettings: {
|
||||
asScopedToClient: deps.uiSettings.asScopedToClient,
|
||||
},
|
||||
auditTrail: deps.auditTrail,
|
||||
coreUsageData: deps.coreUsageData,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -198,38 +198,6 @@ 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)
|
||||
|
@ -499,8 +467,6 @@ export interface CoreServicesUsageData {
|
|||
|
||||
// @public
|
||||
export interface CoreSetup<TPluginsStart extends object = object, TStart = unknown> {
|
||||
// (undocumented)
|
||||
auditTrail: AuditTrailSetup;
|
||||
// (undocumented)
|
||||
capabilities: CapabilitiesSetup;
|
||||
// (undocumented)
|
||||
|
@ -527,8 +493,6 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
|
||||
// @public
|
||||
export interface CoreStart {
|
||||
// (undocumented)
|
||||
auditTrail: AuditTrailStart;
|
||||
// (undocumented)
|
||||
capabilities: CapabilitiesStart;
|
||||
// @internal (undocumented)
|
||||
|
@ -1348,7 +1312,7 @@ export interface LegacyCallAPIOptions {
|
|||
|
||||
// @public @deprecated
|
||||
export class LegacyClusterClient implements ILegacyClusterClient {
|
||||
constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders);
|
||||
constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders);
|
||||
asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient;
|
||||
callAsInternalUser: LegacyAPICaller;
|
||||
close(): void;
|
||||
|
@ -1396,7 +1360,7 @@ export interface LegacyRequest extends Request {
|
|||
|
||||
// @public @deprecated
|
||||
export class LegacyScopedClusterClient implements ILegacyScopedClusterClient {
|
||||
constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined);
|
||||
constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined);
|
||||
callAsCurrentUser(endpoint: string, clientParams?: Record<string, any>, options?: LegacyCallAPIOptions): Promise<any>;
|
||||
callAsInternalUser(endpoint: string, clientParams?: Record<string, any>, options?: LegacyCallAPIOptions): Promise<any>;
|
||||
}
|
||||
|
@ -1738,7 +1702,6 @@ export interface RequestHandlerContext {
|
|||
uiSettings: {
|
||||
client: IUiSettingsClient;
|
||||
};
|
||||
auditor: Auditor;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -100,9 +100,3 @@ 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),
|
||||
}));
|
||||
|
|
|
@ -31,7 +31,6 @@ import {
|
|||
mockMetricsService,
|
||||
mockStatusService,
|
||||
mockLoggingService,
|
||||
mockAuditTrailService,
|
||||
} from './server.test.mocks';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
@ -71,7 +70,6 @@ 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();
|
||||
|
||||
|
@ -85,7 +83,6 @@ 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 () => {
|
||||
|
@ -126,7 +123,6 @@ 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();
|
||||
|
||||
|
@ -135,7 +131,6 @@ 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 () => {
|
||||
|
@ -160,7 +155,6 @@ 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();
|
||||
|
||||
|
@ -173,7 +167,6 @@ 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 () => {
|
||||
|
@ -227,7 +220,6 @@ test(`doesn't validate config if env.isDevClusterMaster is true`, async () => {
|
|||
|
||||
expect(mockEnsureValidConfiguration).not.toHaveBeenCalled();
|
||||
expect(mockContextService.setup).toHaveBeenCalled();
|
||||
expect(mockAuditTrailService.setup).toHaveBeenCalled();
|
||||
expect(mockHttpService.setup).toHaveBeenCalled();
|
||||
expect(mockElasticsearchService.setup).toHaveBeenCalled();
|
||||
expect(mockSavedObjectsService.setup).toHaveBeenCalled();
|
||||
|
|
|
@ -21,7 +21,6 @@ import { config as pathConfig } from '@kbn/utils';
|
|||
import { mapToObject } from '@kbn/std';
|
||||
import { ConfigService, Env, RawConfigurationProvider, 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';
|
||||
|
@ -72,7 +71,6 @@ export class Server {
|
|||
private readonly status: StatusService;
|
||||
private readonly logging: LoggingService;
|
||||
private readonly coreApp: CoreApp;
|
||||
private readonly auditTrail: AuditTrailService;
|
||||
private readonly coreUsageData: CoreUsageDataService;
|
||||
|
||||
#pluginsInitialized?: boolean;
|
||||
|
@ -103,7 +101,6 @@ 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);
|
||||
this.coreUsageData = new CoreUsageDataService(core);
|
||||
}
|
||||
|
@ -139,8 +136,6 @@ export class Server {
|
|||
]),
|
||||
});
|
||||
|
||||
const auditTrailSetup = this.auditTrail.setup();
|
||||
|
||||
const httpSetup = await this.http.setup({
|
||||
context: contextServiceSetup,
|
||||
});
|
||||
|
@ -200,7 +195,6 @@ export class Server {
|
|||
uiSettings: uiSettingsSetup,
|
||||
rendering: renderingSetup,
|
||||
httpResources: httpResourcesSetup,
|
||||
auditTrail: auditTrailSetup,
|
||||
logging: loggingSetup,
|
||||
metrics: metricsSetup,
|
||||
};
|
||||
|
@ -225,11 +219,7 @@ export class Server {
|
|||
this.log.debug('starting server');
|
||||
const startTransaction = apm.startTransaction('server_start', 'kibana_platform');
|
||||
|
||||
const auditTrailStart = this.auditTrail.start();
|
||||
|
||||
const elasticsearchStart = await this.elasticsearch.start({
|
||||
auditTrail: auditTrailStart,
|
||||
});
|
||||
const elasticsearchStart = await this.elasticsearch.start();
|
||||
const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration');
|
||||
const savedObjectsStart = await this.savedObjects.start({
|
||||
elasticsearch: elasticsearchStart,
|
||||
|
@ -252,7 +242,6 @@ export class Server {
|
|||
metrics: metricsStart,
|
||||
savedObjects: savedObjectsStart,
|
||||
uiSettings: uiSettingsStart,
|
||||
auditTrail: auditTrailStart,
|
||||
coreUsageData: coreUsageDataStart,
|
||||
};
|
||||
|
||||
|
@ -285,7 +274,6 @@ export class Server {
|
|||
await this.metrics.stop();
|
||||
await this.status.stop();
|
||||
await this.logging.stop();
|
||||
await this.auditTrail.stop();
|
||||
}
|
||||
|
||||
private registerCoreContext(coreSetup: InternalCoreSetup) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import { LegacyAuditLogger } from '../../../security/server';
|
||||
|
||||
export enum AuthorizationResult {
|
||||
Unauthorized = 'Unauthorized',
|
||||
|
@ -12,9 +12,9 @@ export enum AuthorizationResult {
|
|||
}
|
||||
|
||||
export class ActionsAuthorizationAuditLogger {
|
||||
private readonly auditLogger: AuditLogger;
|
||||
private readonly auditLogger: LegacyAuditLogger;
|
||||
|
||||
constructor(auditLogger: AuditLogger = { log() {} }) {
|
||||
constructor(auditLogger: LegacyAuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { securityMock } from '../../security/server/mocks';
|
|||
import { PluginStartContract as ActionsStartContract } from '../../actions/server';
|
||||
import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { AuditLogger } from '../../security/server';
|
||||
import { LegacyAuditLogger } from '../../security/server';
|
||||
import { ALERTS_FEATURE_ID } from '../common';
|
||||
import { eventLogMock } from '../../event_log/server/mocks';
|
||||
|
||||
|
@ -85,7 +85,7 @@ test('creates an alerts client with proper constructor arguments when security i
|
|||
|
||||
const logger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
} as jest.Mocked<LegacyAuditLogger>;
|
||||
securityPluginSetup.audit.getLogger.mockReturnValue(logger);
|
||||
|
||||
factory.create(request, savedObjectsService);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import { LegacyAuditLogger } from '../../../security/server';
|
||||
|
||||
export enum ScopeType {
|
||||
Consumer,
|
||||
|
@ -17,9 +17,9 @@ export enum AuthorizationResult {
|
|||
}
|
||||
|
||||
export class AlertsAuthorizationAuditLogger {
|
||||
private readonly auditLogger: AuditLogger;
|
||||
private readonly auditLogger: LegacyAuditLogger;
|
||||
|
||||
constructor(auditLogger: AuditLogger = { log() {} }) {
|
||||
constructor(auditLogger: LegacyAuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"id": "auditTrail",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "audit_trail"],
|
||||
"server": true,
|
||||
"ui": false,
|
||||
"requiredPlugins": ["licensing", "security"],
|
||||
"optionalPlugins": ["spaces"]
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* 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<AuditEvent>;
|
||||
const deps = {
|
||||
getCurrentUser: securityMock.createSetup().authc.getCurrentUser,
|
||||
getSpaceId: spacesMock.createSetup().spacesService.getSpaceId,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
event$ = new Subject();
|
||||
client = new AuditTrailClient(
|
||||
httpServerMock.createKibanaRequest({
|
||||
kibanaRequestState: { requestId: 'request id alpha', requestUuid: 'ignore-me' },
|
||||
}),
|
||||
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('populates requestId', (done) => {
|
||||
client.withAuditScope('scope_name');
|
||||
event$.subscribe((event) => {
|
||||
expect(event.requestId).toBe('request id alpha');
|
||||
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"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* 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<AuditEvent>,
|
||||
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,
|
||||
requestId: this.request.id,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
});
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* 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<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<AuditTrailConfigType> = {
|
||||
schema: configSchema,
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
};
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* 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<typeof coreMock.createPluginInitializerContext>;
|
||||
let coreSetup: ReturnType<typeof coreMock.createSetup>;
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* 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<AuditTrailConfigType>;
|
||||
private readonly event$ = new Subject<AuditEvent>();
|
||||
|
||||
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<LoggerContextConfigInput>(
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
requestId?: string;
|
||||
}
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger, AuthenticatedUser } from '../../../security/server';
|
||||
import { LegacyAuditLogger, AuthenticatedUser } from '../../../security/server';
|
||||
import { SavedObjectDescriptor, descriptorToArray } from '../crypto';
|
||||
|
||||
/**
|
||||
* Represents all audit events the plugin can log.
|
||||
*/
|
||||
export class EncryptedSavedObjectsAuditLogger {
|
||||
constructor(private readonly logger: AuditLogger = { log() {} }) {}
|
||||
constructor(private readonly logger: LegacyAuditLogger = { log() {} }) {}
|
||||
|
||||
public encryptAttributeFailure(
|
||||
attributeName: string,
|
||||
|
|
|
@ -1,3 +1,92 @@
|
|||
# Kibana Security Plugin
|
||||
|
||||
See [Configuring security in Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html).
|
||||
See [Configuring security in
|
||||
Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html).
|
||||
|
||||
## Audit logging
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const auditLogger = securitySetup.audit.asScoped(request);
|
||||
auditLogger.log({
|
||||
message: 'User is updating dashboard [id=123]',
|
||||
event: {
|
||||
action: 'saved_object_update',
|
||||
category: EventCategory.DATABASE,
|
||||
type: EventType.CHANGE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
},
|
||||
kibana: {
|
||||
saved_object: { type: 'dashboard', id: '123' },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### What events should be logged?
|
||||
|
||||
The purpose of an audit log is to support compliance, accountability and
|
||||
security by capturing who performed an action, what action was performed and
|
||||
when it occurred. It is not the purpose of an audit log to aid with debugging
|
||||
the system or provide usage statistics.
|
||||
|
||||
**Kibana guidelines:**
|
||||
|
||||
Each API call to Kibana will result in a record in the audit log that captures
|
||||
general information about the request (`http_request` event).
|
||||
|
||||
In addition to that, any operation that is performed on a resource owned by
|
||||
Kibana (e.g. saved objects) and that falls in the following categories, should
|
||||
be included in the audit log:
|
||||
|
||||
- System access (incl. failed attempts due to authentication errors)
|
||||
- Data reads (incl. failed attempts due to authorisation errors)
|
||||
- Data writes (incl. failed attempts due to authorisation errors)
|
||||
|
||||
If Kibana does not own the resource (e.g. when running queries against user
|
||||
indices), then auditing responsibilities are deferred to Elasticsearch and no
|
||||
additional events will be logged.
|
||||
|
||||
**Examples:**
|
||||
|
||||
For a list of audit events that Kibana currently logs see:
|
||||
`docs/user/security/audit-logging.asciidoc`
|
||||
|
||||
### When should an event be logged?
|
||||
|
||||
Due to the asynchronous nature of most operations in Kibana, there is an
|
||||
inherent tradeoff between the following logging approaches:
|
||||
|
||||
- Logging the **intention** before performing an operation, leading to false
|
||||
positives if the operation fails downstream.
|
||||
- Logging the **outcome** after completing an operation, leading to missing
|
||||
records if Kibana crashes before the response is received.
|
||||
- Logging **both**, intention and outcome, leading to unnecessary duplication
|
||||
and noisy/difficult to analyse logs.
|
||||
|
||||
**Kibana guidelines:**
|
||||
|
||||
- **Write operations** should be logged immediately after all authorisation
|
||||
checks have passed, but before the response is received (logging the
|
||||
intention). This ensures that a record of every operation is persisted even in
|
||||
case of an unexpected error.
|
||||
- **Read operations**, on the other hand, should be logged after the operation
|
||||
completed (logging the outcome) since we won't know what resources were
|
||||
accessed before receiving the response.
|
||||
- Be explicit about the timing and outcome of an action in your messaging. (e.g.
|
||||
"User has logged in" vs. "User is creating dashboard")
|
||||
|
||||
### Can an action trigger multiple events?
|
||||
|
||||
- A request to Kibana can perform a combination of different operations, each of
|
||||
which should be captured as separate events.
|
||||
- Operations that are performed on multiple resources (**bulk operations**)
|
||||
should be logged as separate events, one for each resource.
|
||||
- Actions that kick off **background tasks** should be logged as separate
|
||||
events, one for creating the task and another one for executing it.
|
||||
- **Internal checks**, which have been carried out in order to perform an
|
||||
operation, or **errors** that occured as a result of an operation should be
|
||||
logged as an outcome of the operation itself, using the ECS `event.outcome`
|
||||
and `error` fields, instead of logging a separate event.
|
||||
- Multiple events that were part of the same request can be correlated in the
|
||||
audit log using the ECS `trace.id` property.
|
||||
|
|
204
x-pack/plugins/security/server/audit/audit_events.test.ts
Normal file
204
x-pack/plugins/security/server/audit/audit_events.test.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventOutcome,
|
||||
SavedObjectAction,
|
||||
savedObjectEvent,
|
||||
userLoginEvent,
|
||||
httpRequestEvent,
|
||||
} from './audit_events';
|
||||
import { AuthenticationResult } from '../authentication';
|
||||
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
|
||||
import { httpServerMock } from 'src/core/server/mocks';
|
||||
|
||||
describe('#savedObjectEvent', () => {
|
||||
test('creates event with `unknown` outcome', () => {
|
||||
expect(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' },
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"event": Object {
|
||||
"action": "saved_object_create",
|
||||
"category": "database",
|
||||
"outcome": "unknown",
|
||||
"type": "creation",
|
||||
},
|
||||
"kibana": Object {
|
||||
"add_to_spaces": undefined,
|
||||
"delete_from_spaces": undefined,
|
||||
"saved_object": Object {
|
||||
"id": "SAVED_OBJECT_ID",
|
||||
"type": "dashboard",
|
||||
},
|
||||
},
|
||||
"message": "User is creating dashboard [id=SAVED_OBJECT_ID]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates event with `success` outcome', () => {
|
||||
expect(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' },
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"event": Object {
|
||||
"action": "saved_object_create",
|
||||
"category": "database",
|
||||
"outcome": "success",
|
||||
"type": "creation",
|
||||
},
|
||||
"kibana": Object {
|
||||
"add_to_spaces": undefined,
|
||||
"delete_from_spaces": undefined,
|
||||
"saved_object": Object {
|
||||
"id": "SAVED_OBJECT_ID",
|
||||
"type": "dashboard",
|
||||
},
|
||||
},
|
||||
"message": "User has created dashboard [id=SAVED_OBJECT_ID]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates event with `failure` outcome', () => {
|
||||
expect(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' },
|
||||
error: new Error('ERROR_MESSAGE'),
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "ERROR_MESSAGE",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "saved_object_create",
|
||||
"category": "database",
|
||||
"outcome": "failure",
|
||||
"type": "creation",
|
||||
},
|
||||
"kibana": Object {
|
||||
"add_to_spaces": undefined,
|
||||
"delete_from_spaces": undefined,
|
||||
"saved_object": Object {
|
||||
"id": "SAVED_OBJECT_ID",
|
||||
"type": "dashboard",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to create dashboard [id=SAVED_OBJECT_ID]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#userLoginEvent', () => {
|
||||
test('creates event with `success` outcome', () => {
|
||||
expect(
|
||||
userLoginEvent({
|
||||
authenticationResult: AuthenticationResult.succeeded(mockAuthenticatedUser()),
|
||||
authenticationProvider: 'basic1',
|
||||
authenticationType: 'basic',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"event": Object {
|
||||
"action": "user_login",
|
||||
"category": "authentication",
|
||||
"outcome": "success",
|
||||
},
|
||||
"kibana": Object {
|
||||
"authentication_provider": "basic1",
|
||||
"authentication_realm": "native1",
|
||||
"authentication_type": "basic",
|
||||
"lookup_realm": "native1",
|
||||
"space_id": undefined,
|
||||
},
|
||||
"message": "User [user] has logged in using basic provider [name=basic1]",
|
||||
"user": Object {
|
||||
"name": "user",
|
||||
"roles": Array [
|
||||
"user-role",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates event with `failure` outcome', () => {
|
||||
expect(
|
||||
userLoginEvent({
|
||||
authenticationResult: AuthenticationResult.failed(new Error('Not Authorized')),
|
||||
authenticationProvider: 'basic1',
|
||||
authenticationType: 'basic',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "Not Authorized",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "user_login",
|
||||
"category": "authentication",
|
||||
"outcome": "failure",
|
||||
},
|
||||
"kibana": Object {
|
||||
"authentication_provider": "basic1",
|
||||
"authentication_realm": undefined,
|
||||
"authentication_type": "basic",
|
||||
"lookup_realm": undefined,
|
||||
"space_id": undefined,
|
||||
},
|
||||
"message": "Failed attempt to login using basic provider [name=basic1]",
|
||||
"user": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#httpRequestEvent', () => {
|
||||
test('creates event with `unknown` outcome', () => {
|
||||
expect(
|
||||
httpRequestEvent({
|
||||
request: httpServerMock.createKibanaRequest(),
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "http_request",
|
||||
"category": "web",
|
||||
"outcome": "unknown",
|
||||
},
|
||||
"http": Object {
|
||||
"request": Object {
|
||||
"method": "get",
|
||||
},
|
||||
},
|
||||
"message": "User is requesting [/path] endpoint",
|
||||
"url": Object {
|
||||
"domain": undefined,
|
||||
"path": "/path",
|
||||
"port": undefined,
|
||||
"query": undefined,
|
||||
"scheme": undefined,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
244
x-pack/plugins/security/server/audit/audit_events.ts
Normal file
244
x-pack/plugins/security/server/audit/audit_events.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { AuthenticationResult } from '../authentication/authentication_result';
|
||||
|
||||
/**
|
||||
* Audit event schema using ECS format.
|
||||
* https://www.elastic.co/guide/en/ecs/1.5/index.html
|
||||
* @public
|
||||
*/
|
||||
export interface AuditEvent {
|
||||
/**
|
||||
* Human readable message describing action, outcome and user.
|
||||
*
|
||||
* @example
|
||||
* User [jdoe] logged in using basic provider [name=basic1]
|
||||
*/
|
||||
message: string;
|
||||
event: {
|
||||
action: string;
|
||||
category?: EventCategory;
|
||||
type?: EventType;
|
||||
outcome?: EventOutcome;
|
||||
module?: string;
|
||||
dataset?: string;
|
||||
};
|
||||
user?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
full_name?: string;
|
||||
hash?: string;
|
||||
roles?: readonly string[];
|
||||
};
|
||||
kibana?: {
|
||||
/**
|
||||
* Current space id of the request.
|
||||
*/
|
||||
space_id?: string;
|
||||
/**
|
||||
* Saved object that was created, changed, deleted or accessed as part of the action.
|
||||
*/
|
||||
saved_object?: {
|
||||
type: string;
|
||||
id?: string;
|
||||
};
|
||||
/**
|
||||
* Any additional event specific fields.
|
||||
*/
|
||||
[x: string]: any;
|
||||
};
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
http?: {
|
||||
request?: {
|
||||
method?: string;
|
||||
body?: {
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
response?: {
|
||||
status_code?: number;
|
||||
};
|
||||
};
|
||||
url?: {
|
||||
domain?: string;
|
||||
full?: string;
|
||||
path?: string;
|
||||
port?: number;
|
||||
query?: string;
|
||||
scheme?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum EventCategory {
|
||||
DATABASE = 'database',
|
||||
WEB = 'web',
|
||||
IAM = 'iam',
|
||||
AUTHENTICATION = 'authentication',
|
||||
PROCESS = 'process',
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
USER = 'user',
|
||||
GROUP = 'group',
|
||||
CREATION = 'creation',
|
||||
ACCESS = 'access',
|
||||
CHANGE = 'change',
|
||||
DELETION = 'deletion',
|
||||
}
|
||||
|
||||
export enum EventOutcome {
|
||||
SUCCESS = 'success',
|
||||
FAILURE = 'failure',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
export interface HttpRequestParams {
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent {
|
||||
const { pathname, search } = request.url;
|
||||
|
||||
return {
|
||||
message: `User is requesting [${pathname}] endpoint`,
|
||||
event: {
|
||||
action: 'http_request',
|
||||
category: EventCategory.WEB,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
},
|
||||
http: {
|
||||
request: {
|
||||
method: request.route.method,
|
||||
},
|
||||
},
|
||||
url: {
|
||||
domain: request.url.hostname,
|
||||
path: pathname,
|
||||
port: request.url.port ? parseInt(request.url.port, 10) : undefined,
|
||||
query: search?.slice(1) || undefined,
|
||||
scheme: request.url.protocol,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserLoginParams {
|
||||
authenticationResult: AuthenticationResult;
|
||||
authenticationProvider?: string;
|
||||
authenticationType?: string;
|
||||
}
|
||||
|
||||
export function userLoginEvent({
|
||||
authenticationResult,
|
||||
authenticationProvider,
|
||||
authenticationType,
|
||||
}: UserLoginParams): AuditEvent {
|
||||
return {
|
||||
message: authenticationResult.user
|
||||
? `User [${authenticationResult.user.username}] has logged in using ${authenticationType} provider [name=${authenticationProvider}]`
|
||||
: `Failed attempt to login using ${authenticationType} provider [name=${authenticationProvider}]`,
|
||||
event: {
|
||||
action: 'user_login',
|
||||
category: EventCategory.AUTHENTICATION,
|
||||
outcome: authenticationResult.user ? EventOutcome.SUCCESS : EventOutcome.FAILURE,
|
||||
},
|
||||
user: authenticationResult.user && {
|
||||
name: authenticationResult.user.username,
|
||||
roles: authenticationResult.user.roles,
|
||||
},
|
||||
kibana: {
|
||||
space_id: undefined, // Ensure this does not get populated by audit service
|
||||
authentication_provider: authenticationProvider,
|
||||
authentication_type: authenticationType,
|
||||
authentication_realm: authenticationResult.user?.authentication_realm.name,
|
||||
lookup_realm: authenticationResult.user?.lookup_realm.name,
|
||||
},
|
||||
error: authenticationResult.error && {
|
||||
code: authenticationResult.error.name,
|
||||
message: authenticationResult.error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export enum SavedObjectAction {
|
||||
CREATE = 'saved_object_create',
|
||||
GET = 'saved_object_get',
|
||||
UPDATE = 'saved_object_update',
|
||||
DELETE = 'saved_object_delete',
|
||||
FIND = 'saved_object_find',
|
||||
ADD_TO_SPACES = 'saved_object_add_to_spaces',
|
||||
DELETE_FROM_SPACES = 'saved_object_delete_from_spaces',
|
||||
}
|
||||
|
||||
const eventVerbs = {
|
||||
saved_object_create: ['create', 'creating', 'created'],
|
||||
saved_object_get: ['access', 'accessing', 'accessed'],
|
||||
saved_object_update: ['update', 'updating', 'updated'],
|
||||
saved_object_delete: ['delete', 'deleting', 'deleted'],
|
||||
saved_object_find: ['access', 'accessing', 'accessed'],
|
||||
saved_object_add_to_spaces: ['update', 'updating', 'updated'],
|
||||
saved_object_delete_from_spaces: ['update', 'updating', 'updated'],
|
||||
};
|
||||
|
||||
const eventTypes = {
|
||||
saved_object_create: EventType.CREATION,
|
||||
saved_object_get: EventType.ACCESS,
|
||||
saved_object_update: EventType.CHANGE,
|
||||
saved_object_delete: EventType.DELETION,
|
||||
saved_object_find: EventType.ACCESS,
|
||||
saved_object_add_to_spaces: EventType.CHANGE,
|
||||
saved_object_delete_from_spaces: EventType.CHANGE,
|
||||
};
|
||||
|
||||
export interface SavedObjectParams {
|
||||
action: SavedObjectAction;
|
||||
outcome?: EventOutcome;
|
||||
savedObject?: Required<Required<AuditEvent>['kibana']>['saved_object'];
|
||||
addToSpaces?: readonly string[];
|
||||
deleteFromSpaces?: readonly string[];
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export function savedObjectEvent({
|
||||
action,
|
||||
savedObject,
|
||||
addToSpaces,
|
||||
deleteFromSpaces,
|
||||
outcome,
|
||||
error,
|
||||
}: SavedObjectParams): AuditEvent {
|
||||
const doc = savedObject ? `${savedObject.type} [id=${savedObject.id}]` : 'saved objects';
|
||||
const [present, progressive, past] = eventVerbs[action];
|
||||
const message = error
|
||||
? `Failed attempt to ${present} ${doc}`
|
||||
: outcome === 'unknown'
|
||||
? `User is ${progressive} ${doc}`
|
||||
: `User has ${past} ${doc}`;
|
||||
const type = eventTypes[action];
|
||||
|
||||
return {
|
||||
message,
|
||||
event: {
|
||||
action,
|
||||
category: EventCategory.DATABASE,
|
||||
type,
|
||||
outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS),
|
||||
},
|
||||
kibana: {
|
||||
saved_object: savedObject,
|
||||
add_to_spaces: addToSpaces,
|
||||
delete_from_spaces: deleteFromSpaces,
|
||||
},
|
||||
error: error && {
|
||||
code: error.name,
|
||||
message: error.message,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -3,163 +3,506 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { AuditService } from './audit_service';
|
||||
import { loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { AuditService, filterEvent, createLoggingConfig } from './audit_service';
|
||||
import { AuditEvent, EventCategory, EventType, EventOutcome } from './audit_events';
|
||||
import {
|
||||
coreMock,
|
||||
loggingSystemMock,
|
||||
httpServiceMock,
|
||||
httpServerMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { licenseMock } from '../../common/licensing/index.mock';
|
||||
import { ConfigSchema, ConfigType } from '../config';
|
||||
import { SecurityLicenseFeatures } from '../../common/licensing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
|
||||
const createConfig = (settings: Partial<ConfigType['audit']>) => {
|
||||
return ConfigSchema.validate(settings);
|
||||
};
|
||||
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
const config = createConfig({ enabled: true });
|
||||
const { logging } = coreMock.createSetup();
|
||||
const http = httpServiceMock.createSetupContract();
|
||||
const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] });
|
||||
const getSpaceId = jest.fn().mockReturnValue('default');
|
||||
|
||||
beforeEach(() => {
|
||||
logger.info.mockClear();
|
||||
logging.configure.mockClear();
|
||||
http.registerOnPostAuth.mockClear();
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
it('returns the expected contract', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const auditService = new AuditService(logger);
|
||||
const license = licenseMock.create();
|
||||
expect(auditService.setup({ license, config })).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
auditService.setup({
|
||||
license,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"asScoped": [Function],
|
||||
"getLogger": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test(`calls the underlying logger with the provided message and requisite tags`, () => {
|
||||
const pluginId = 'foo';
|
||||
it('configures logging correctly when using ecs logger', async () => {
|
||||
new AuditService(logger).setup({
|
||||
license,
|
||||
config: {
|
||||
enabled: true,
|
||||
appender: {
|
||||
kind: 'console',
|
||||
layout: {
|
||||
kind: 'pattern',
|
||||
},
|
||||
},
|
||||
},
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable));
|
||||
});
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
license.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
it('does not configure logging when using legacy logger', async () => {
|
||||
new AuditService(logger).setup({
|
||||
license,
|
||||
config: {
|
||||
enabled: true,
|
||||
},
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
expect(logging.configure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const auditService = new AuditService(logger).setup({ license, config });
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith(message, {
|
||||
eventType,
|
||||
tags: [pluginId, eventType],
|
||||
it('registers post auth hook', () => {
|
||||
new AuditService(logger).setup({
|
||||
license,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
test(`calls the underlying logger with the provided metadata`, () => {
|
||||
const pluginId = 'foo';
|
||||
describe('#asScoped', () => {
|
||||
it('logs event enriched with meta data', async () => {
|
||||
const audit = new AuditService(logger).setup({
|
||||
license,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' },
|
||||
});
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
license.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({ license, config });
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
const metadata = Object.freeze({
|
||||
property1: 'value1',
|
||||
property2: false,
|
||||
property3: 123,
|
||||
audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } });
|
||||
expect(logger.info).toHaveBeenCalledWith('MESSAGE', {
|
||||
event: { action: 'ACTION' },
|
||||
kibana: { space_id: 'default' },
|
||||
message: 'MESSAGE',
|
||||
trace: { id: 'REQUEST_ID' },
|
||||
user: { name: 'jdoe', roles: ['admin'] },
|
||||
});
|
||||
});
|
||||
auditLogger.log(eventType, message, metadata);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith(message, {
|
||||
eventType,
|
||||
tags: [pluginId, eventType],
|
||||
property1: 'value1',
|
||||
property2: false,
|
||||
property3: 123,
|
||||
it('does not log to audit logger if event matches ignore filter', async () => {
|
||||
const audit = new AuditService(logger).setup({
|
||||
license,
|
||||
config: {
|
||||
enabled: true,
|
||||
ignore_filters: [{ actions: ['ACTION'] }],
|
||||
},
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' },
|
||||
});
|
||||
|
||||
audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } });
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test(`does not call the underlying logger if license does not support audit logging`, () => {
|
||||
const pluginId = 'foo';
|
||||
describe('#createLoggingConfig', () => {
|
||||
test('sets log level to `info` when audit logging is enabled and appender is defined', async () => {
|
||||
const features$ = of({
|
||||
allowAuditLogging: true,
|
||||
});
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
license.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: false,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
const loggingConfig = await features$
|
||||
.pipe(
|
||||
createLoggingConfig({
|
||||
enabled: true,
|
||||
appender: {
|
||||
kind: 'console',
|
||||
layout: {
|
||||
kind: 'pattern',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
const auditService = new AuditService(logger).setup({ license, config });
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`does not call the underlying logger if security audit logging is not enabled`, () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
license.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license,
|
||||
config: createConfig({
|
||||
enabled: false,
|
||||
}),
|
||||
expect(loggingConfig).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"appenders": Object {
|
||||
"auditTrailAppender": Object {
|
||||
"kind": "console",
|
||||
"layout": Object {
|
||||
"kind": "pattern",
|
||||
},
|
||||
},
|
||||
},
|
||||
"loggers": Array [
|
||||
Object {
|
||||
"appenders": Array [
|
||||
"auditTrailAppender",
|
||||
],
|
||||
"context": "audit",
|
||||
"level": "info",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
test('sets log level to `off` when audit logging is disabled', async () => {
|
||||
const features$ = of({
|
||||
allowAuditLogging: true,
|
||||
});
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
const loggingConfig = await features$
|
||||
.pipe(
|
||||
createLoggingConfig({
|
||||
enabled: false,
|
||||
appender: {
|
||||
kind: 'console',
|
||||
layout: {
|
||||
kind: 'pattern',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
expect(loggingConfig.loggers![0].level).toEqual('off');
|
||||
});
|
||||
|
||||
test('sets log level to `off` when appender is not defined', async () => {
|
||||
const features$ = of({
|
||||
allowAuditLogging: true,
|
||||
});
|
||||
|
||||
const loggingConfig = await features$
|
||||
.pipe(
|
||||
createLoggingConfig({
|
||||
enabled: true,
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(loggingConfig.loggers![0].level).toEqual('off');
|
||||
});
|
||||
|
||||
test('sets log level to `off` when license does not allow audit logging', async () => {
|
||||
const features$ = of({
|
||||
allowAuditLogging: false,
|
||||
});
|
||||
|
||||
const loggingConfig = await features$
|
||||
.pipe(
|
||||
createLoggingConfig({
|
||||
enabled: true,
|
||||
appender: {
|
||||
kind: 'console',
|
||||
layout: {
|
||||
kind: 'pattern',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(loggingConfig.loggers![0].level).toEqual('off');
|
||||
});
|
||||
});
|
||||
|
||||
test(`calls the underlying logger after license upgrade`, () => {
|
||||
const pluginId = 'foo';
|
||||
describe('#filterEvent', () => {
|
||||
const event: AuditEvent = {
|
||||
message: 'this is my audit message',
|
||||
event: {
|
||||
action: 'http_request',
|
||||
category: EventCategory.WEB,
|
||||
type: EventType.ACCESS,
|
||||
outcome: EventOutcome.SUCCESS,
|
||||
},
|
||||
user: {
|
||||
name: 'jdoe',
|
||||
},
|
||||
kibana: {
|
||||
space_id: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const license = licenseMock.create();
|
||||
test('keeps event when ignore filters are undefined or empty', () => {
|
||||
expect(filterEvent(event, undefined)).toBeTruthy();
|
||||
expect(filterEvent(event, [])).toBeTruthy();
|
||||
});
|
||||
|
||||
const features$ = new BehaviorSubject({
|
||||
allowAuditLogging: false,
|
||||
} as SecurityLicenseFeatures);
|
||||
test('filters event correctly when a single match is found per criteria', () => {
|
||||
expect(filterEvent(event, [{ actions: ['NO_MATCH'] }])).toBeTruthy();
|
||||
expect(filterEvent(event, [{ actions: ['NO_MATCH', 'http_request'] }])).toBeFalsy();
|
||||
expect(filterEvent(event, [{ categories: ['NO_MATCH', 'web'] }])).toBeFalsy();
|
||||
expect(filterEvent(event, [{ types: ['NO_MATCH', 'access'] }])).toBeFalsy();
|
||||
expect(filterEvent(event, [{ outcomes: ['NO_MATCH', 'success'] }])).toBeFalsy();
|
||||
expect(filterEvent(event, [{ spaces: ['NO_MATCH', 'default'] }])).toBeFalsy();
|
||||
});
|
||||
|
||||
license.features$ = features$.asObservable();
|
||||
test('keeps event when one criteria per rule does not match', () => {
|
||||
expect(
|
||||
filterEvent(event, [
|
||||
{
|
||||
actions: ['NO_MATCH'],
|
||||
categories: ['web'],
|
||||
types: ['access'],
|
||||
outcomes: ['success'],
|
||||
spaces: ['default'],
|
||||
},
|
||||
{
|
||||
actions: ['http_request'],
|
||||
categories: ['NO_MATCH'],
|
||||
types: ['access'],
|
||||
outcomes: ['success'],
|
||||
spaces: ['default'],
|
||||
},
|
||||
{
|
||||
actions: ['http_request'],
|
||||
categories: ['web'],
|
||||
types: ['NO_MATCH'],
|
||||
outcomes: ['success'],
|
||||
spaces: ['default'],
|
||||
},
|
||||
{
|
||||
actions: ['http_request'],
|
||||
categories: ['web'],
|
||||
types: ['access'],
|
||||
outcomes: ['NO_MATCH'],
|
||||
spaces: ['default'],
|
||||
},
|
||||
{
|
||||
actions: ['http_request'],
|
||||
categories: ['web'],
|
||||
types: ['access'],
|
||||
outcomes: ['success'],
|
||||
spaces: ['NO_MATCH'],
|
||||
},
|
||||
])
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
const auditService = new AuditService(logger).setup({ license, config });
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
|
||||
// perform license upgrade
|
||||
features$.next({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures);
|
||||
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
test('filters out event when all criteria in a single rule match', () => {
|
||||
expect(
|
||||
filterEvent(event, [
|
||||
{
|
||||
actions: ['NO_MATCH'],
|
||||
categories: ['NO_MATCH'],
|
||||
types: ['NO_MATCH'],
|
||||
outcomes: ['NO_MATCH'],
|
||||
spaces: ['NO_MATCH'],
|
||||
},
|
||||
{
|
||||
actions: ['http_request'],
|
||||
categories: ['web'],
|
||||
types: ['access'],
|
||||
outcomes: ['success'],
|
||||
spaces: ['default'],
|
||||
},
|
||||
])
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLogger', () => {
|
||||
test('calls the underlying logger with the provided message and requisite tags', () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const licenseWithFeatures = licenseMock.create();
|
||||
licenseWithFeatures.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license: licenseWithFeatures,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith(message, {
|
||||
eventType,
|
||||
tags: [pluginId, eventType],
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the underlying logger with the provided metadata', () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const licenseWithFeatures = licenseMock.create();
|
||||
licenseWithFeatures.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license: licenseWithFeatures,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
const metadata = Object.freeze({
|
||||
property1: 'value1',
|
||||
property2: false,
|
||||
property3: 123,
|
||||
});
|
||||
auditLogger.log(eventType, message, metadata);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith(message, {
|
||||
eventType,
|
||||
tags: [pluginId, eventType],
|
||||
property1: 'value1',
|
||||
property2: false,
|
||||
property3: 123,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not call the underlying logger if license does not support audit logging', () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const licenseWithFeatures = licenseMock.create();
|
||||
licenseWithFeatures.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: false,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license: licenseWithFeatures,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not call the underlying logger if security audit logging is not enabled', () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const licenseWithFeatures = licenseMock.create();
|
||||
licenseWithFeatures.features$ = new BehaviorSubject({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures).asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license: licenseWithFeatures,
|
||||
config: createConfig({
|
||||
enabled: false,
|
||||
}),
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls the underlying logger after license upgrade', () => {
|
||||
const pluginId = 'foo';
|
||||
|
||||
const licenseWithFeatures = licenseMock.create();
|
||||
|
||||
const features$ = new BehaviorSubject({
|
||||
allowAuditLogging: false,
|
||||
} as SecurityLicenseFeatures);
|
||||
|
||||
licenseWithFeatures.features$ = features$.asObservable();
|
||||
|
||||
const auditService = new AuditService(logger).setup({
|
||||
license: licenseWithFeatures,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
});
|
||||
|
||||
const auditLogger = auditService.getLogger(pluginId);
|
||||
|
||||
const eventType = 'bar';
|
||||
const message = 'this is my audit message';
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
|
||||
// perform license upgrade
|
||||
features$.next({
|
||||
allowAuditLogging: true,
|
||||
} as SecurityLicenseFeatures);
|
||||
|
||||
auditLogger.log(eventType, message);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,53 +5,181 @@
|
|||
*/
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { SecurityLicense } from '../../common/licensing';
|
||||
import { map, distinctUntilKeyChanged } from 'rxjs/operators';
|
||||
import {
|
||||
Logger,
|
||||
LoggingServiceSetup,
|
||||
KibanaRequest,
|
||||
HttpServiceSetup,
|
||||
LoggerContextConfigInput,
|
||||
} from '../../../../../src/core/server';
|
||||
import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing';
|
||||
import { ConfigType } from '../config';
|
||||
import { SpacesPluginSetup } from '../../../spaces/server';
|
||||
import { AuditEvent, httpRequestEvent } from './audit_events';
|
||||
import { SecurityPluginSetup } from '..';
|
||||
|
||||
export interface AuditLogger {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface LegacyAuditLogger {
|
||||
log: (eventType: string, message: string, data?: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export interface AuditLogger {
|
||||
log: (event: AuditEvent) => void;
|
||||
}
|
||||
|
||||
interface AuditLogMeta extends AuditEvent {
|
||||
session?: {
|
||||
id: string;
|
||||
};
|
||||
trace: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuditServiceSetup {
|
||||
getLogger: (id?: string) => AuditLogger;
|
||||
asScoped: (request: KibanaRequest) => AuditLogger;
|
||||
getLogger: (id?: string) => LegacyAuditLogger;
|
||||
}
|
||||
|
||||
interface AuditServiceSetupParams {
|
||||
license: SecurityLicense;
|
||||
config: ConfigType['audit'];
|
||||
logging: Pick<LoggingServiceSetup, 'configure'>;
|
||||
http: Pick<HttpServiceSetup, 'registerOnPostAuth'>;
|
||||
getCurrentUser(
|
||||
request: KibanaRequest
|
||||
): ReturnType<SecurityPluginSetup['authc']['getCurrentUser']> | undefined;
|
||||
getSpaceId(
|
||||
request: KibanaRequest
|
||||
): ReturnType<SpacesPluginSetup['spacesService']['getSpaceId']> | undefined;
|
||||
}
|
||||
|
||||
export class AuditService {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private licenseFeaturesSubscription?: Subscription;
|
||||
private auditLoggingEnabled = false;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private allowAuditLogging = false;
|
||||
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
setup({ license, config }: AuditServiceSetupParams): AuditServiceSetup {
|
||||
if (config.enabled) {
|
||||
setup({
|
||||
license,
|
||||
config,
|
||||
logging,
|
||||
http,
|
||||
getCurrentUser,
|
||||
getSpaceId,
|
||||
}: AuditServiceSetupParams): AuditServiceSetup {
|
||||
if (config.enabled && !config.appender) {
|
||||
this.licenseFeaturesSubscription = license.features$.subscribe(({ allowAuditLogging }) => {
|
||||
this.auditLoggingEnabled = allowAuditLogging;
|
||||
this.allowAuditLogging = allowAuditLogging;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getLogger: (id?: string): AuditLogger => {
|
||||
return {
|
||||
log: (eventType: string, message: string, data?: Record<string, any>) => {
|
||||
if (!this.auditLoggingEnabled) {
|
||||
return;
|
||||
}
|
||||
// Do not change logging for legacy logger
|
||||
if (config.appender) {
|
||||
// Configure logging during setup and when license changes
|
||||
logging.configure(
|
||||
license.features$.pipe(
|
||||
distinctUntilKeyChanged('allowAuditLogging'),
|
||||
createLoggingConfig(config)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(message, {
|
||||
tags: id ? [id, eventType] : [eventType],
|
||||
eventType,
|
||||
...data,
|
||||
});
|
||||
/**
|
||||
* Creates an {@link AuditLogger} scoped to the current request.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const auditLogger = securitySetup.audit.asScoped(request);
|
||||
* auditLogger.log(event);
|
||||
* ```
|
||||
*/
|
||||
const asScoped = (request: KibanaRequest): AuditLogger => {
|
||||
/**
|
||||
* Logs an {@link AuditEvent} and automatically adds meta data about the
|
||||
* current user, space and correlation id.
|
||||
*
|
||||
* Guidelines around what events should be logged and how they should be
|
||||
* structured can be found in: `/x-pack/plugins/security/README.md`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const auditLogger = securitySetup.audit.asScoped(request);
|
||||
* auditLogger.log({
|
||||
* message: 'User is updating dashboard [id=123]',
|
||||
* event: {
|
||||
* action: 'saved_object_update',
|
||||
* outcome: 'unknown'
|
||||
* },
|
||||
* kibana: {
|
||||
* saved_object: { type: 'dashboard', id: '123' }
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
const log = (event: AuditEvent) => {
|
||||
const user = getCurrentUser(request);
|
||||
const spaceId = getSpaceId(request);
|
||||
const meta: AuditLogMeta = {
|
||||
...event,
|
||||
user:
|
||||
(user && {
|
||||
name: user.username,
|
||||
roles: user.roles,
|
||||
}) ||
|
||||
event.user,
|
||||
kibana: {
|
||||
space_id: spaceId,
|
||||
...event.kibana,
|
||||
},
|
||||
trace: {
|
||||
id: request.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
if (filterEvent(meta, config.ignore_filters)) {
|
||||
this.logger.info(event.message!, meta);
|
||||
}
|
||||
};
|
||||
return { log };
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use `audit.asScoped(request)` method instead to create an audit logger
|
||||
*/
|
||||
const getLogger = (id?: string): LegacyAuditLogger => {
|
||||
return {
|
||||
log: (eventType: string, message: string, data?: Record<string, any>) => {
|
||||
if (!this.allowAuditLogging) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(message, {
|
||||
tags: id ? [id, eventType] : [eventType],
|
||||
eventType,
|
||||
...data,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
http.registerOnPostAuth((request, response, t) => {
|
||||
if (request.auth.isAuthenticated) {
|
||||
asScoped(request).log(httpRequestEvent({ request }));
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
|
||||
return { asScoped, getLogger };
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
@ -61,3 +189,40 @@ export class AuditService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createLoggingConfig = (config: ConfigType['audit']) =>
|
||||
map<Pick<SecurityLicenseFeatures, 'allowAuditLogging'>, LoggerContextConfigInput>((features) => ({
|
||||
appenders: {
|
||||
auditTrailAppender: config.appender ?? {
|
||||
kind: 'console',
|
||||
layout: {
|
||||
kind: 'pattern',
|
||||
highlight: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
loggers: [
|
||||
{
|
||||
context: 'audit',
|
||||
level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off',
|
||||
appenders: ['auditTrailAppender'],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
export function filterEvent(
|
||||
event: AuditEvent,
|
||||
ignoreFilters: ConfigType['audit']['ignore_filters']
|
||||
) {
|
||||
if (ignoreFilters) {
|
||||
return !ignoreFilters.some(
|
||||
(rule) =>
|
||||
(!rule.actions || rule.actions.includes(event.event.action)) &&
|
||||
(!rule.categories || rule.categories.includes(event.event.category!)) &&
|
||||
(!rule.types || rule.types.includes(event.event.type!)) &&
|
||||
(!rule.outcomes || rule.outcomes.includes(event.event.outcome!)) &&
|
||||
(!rule.spaces || rule.spaces.includes(event.kibana?.space_id!))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ export const auditServiceMock = {
|
|||
create() {
|
||||
return {
|
||||
getLogger: jest.fn(),
|
||||
asScoped: jest.fn().mockReturnValue({
|
||||
log: jest.fn(),
|
||||
}),
|
||||
} as jest.Mocked<ReturnType<AuditService['setup']>>;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,5 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AuditService, AuditServiceSetup, AuditLogger } from './audit_service';
|
||||
export { AuditService, AuditServiceSetup, AuditLogger, LegacyAuditLogger } from './audit_service';
|
||||
export {
|
||||
AuditEvent,
|
||||
EventCategory,
|
||||
EventType,
|
||||
EventOutcome,
|
||||
userLoginEvent,
|
||||
httpRequestEvent,
|
||||
savedObjectEvent,
|
||||
SavedObjectAction,
|
||||
} from './audit_events';
|
||||
export { SecurityAuditLogger } from './security_audit_logger';
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
*/
|
||||
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import { AuditLogger } from './audit_service';
|
||||
import { LegacyAuditLogger } from './audit_service';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export class SecurityAuditLogger {
|
||||
constructor(private readonly logger: AuditLogger) {}
|
||||
constructor(private readonly logger: LegacyAuditLogger) {}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
savedObjectsAuthorizationFailure(
|
||||
username: string,
|
||||
action: string,
|
||||
|
@ -37,6 +43,9 @@ export class SecurityAuditLogger {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
savedObjectsAuthorizationSuccess(
|
||||
username: string,
|
||||
action: string,
|
||||
|
@ -59,6 +68,9 @@ export class SecurityAuditLogger {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) {
|
||||
this.logger.log(
|
||||
'access_agreement_acknowledged',
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '../../../../../src/core/server/mocks';
|
||||
import { licenseMock } from '../../common/licensing/index.mock';
|
||||
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
|
||||
import { securityAuditLoggerMock } from '../audit/index.mock';
|
||||
import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock';
|
||||
import { sessionMock } from '../session_management/index.mock';
|
||||
import { SecurityLicenseFeatures } from '../../common/licensing';
|
||||
import { ConfigSchema, createConfig } from '../config';
|
||||
|
@ -40,7 +40,8 @@ function getMockOptions({
|
|||
selector?: AuthenticatorOptions['config']['authc']['selector'];
|
||||
} = {}) {
|
||||
return {
|
||||
auditLogger: securityAuditLoggerMock.create(),
|
||||
legacyAuditLogger: securityAuditLoggerMock.create(),
|
||||
audit: auditServiceMock.create(),
|
||||
getCurrentUser: jest.fn(),
|
||||
clusterClient: elasticsearchServiceMock.createLegacyClusterClient(),
|
||||
basePath: httpServiceMock.createSetupContract().basePath,
|
||||
|
@ -215,9 +216,15 @@ describe('Authenticator', () => {
|
|||
let authenticator: Authenticator;
|
||||
let mockOptions: ReturnType<typeof getMockOptions>;
|
||||
let mockSessVal: SessionValue;
|
||||
const auditLogger = {
|
||||
log: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
auditLogger.log.mockClear();
|
||||
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
||||
mockOptions.session.get.mockResolvedValue(null);
|
||||
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
|
||||
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
|
||||
|
||||
authenticator = new Authenticator(mockOptions);
|
||||
|
@ -280,6 +287,49 @@ describe('Authenticator', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('adds audit event when successful.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const user = mockAuthenticatedUser();
|
||||
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
||||
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
||||
);
|
||||
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: { action: 'user_login', category: 'authentication', outcome: 'success' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds audit event when not successful.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const failureReason = new Error('Not Authorized');
|
||||
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
||||
AuthenticationResult.failed(failureReason)
|
||||
);
|
||||
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: { action: 'user_login', category: 'authentication', outcome: 'failure' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add audit event when not handled.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await expect(
|
||||
authenticator.login(request, { provider: { type: 'token' }, value: {} })
|
||||
).resolves.toEqual(AuthenticationResult.notHandled());
|
||||
|
||||
await authenticator.login(request, { provider: { name: 'basic2' }, value: {} });
|
||||
|
||||
expect(auditLogger.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates session whenever authentication provider returns state', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
@ -1859,11 +1909,14 @@ describe('Authenticator', () => {
|
|||
accessAgreementAcknowledged: true,
|
||||
});
|
||||
|
||||
expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith('user', {
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
});
|
||||
expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith(
|
||||
'user',
|
||||
{
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { SecurityLicense } from '../../common/licensing';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import { SecurityAuditLogger } from '../audit';
|
||||
import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit';
|
||||
import { ConfigType } from '../config';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
|
||||
|
@ -59,7 +59,8 @@ export interface ProviderLoginAttempt {
|
|||
}
|
||||
|
||||
export interface AuthenticatorOptions {
|
||||
auditLogger: SecurityAuditLogger;
|
||||
legacyAuditLogger: SecurityAuditLogger;
|
||||
audit: AuditServiceSetup;
|
||||
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
|
||||
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
|
||||
config: Pick<ConfigType, 'authc'>;
|
||||
|
@ -293,6 +294,20 @@ export class Authenticator {
|
|||
existingSessionValue,
|
||||
});
|
||||
|
||||
// Checking for presence of `user` object to determine success state rather than
|
||||
// `success()` method since that indicates a successful authentication and `redirect()`
|
||||
// could also (but does not always) authenticate a user successfully (e.g. SAML flow)
|
||||
if (authenticationResult.user || authenticationResult.failed()) {
|
||||
const auditLogger = this.options.audit.asScoped(request);
|
||||
auditLogger.log(
|
||||
userLoginEvent({
|
||||
authenticationResult,
|
||||
authenticationProvider: providerName,
|
||||
authenticationType: provider.type,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return this.handlePreAccessRedirects(
|
||||
request,
|
||||
authenticationResult,
|
||||
|
@ -421,7 +436,7 @@ export class Authenticator {
|
|||
accessAgreementAcknowledged: true,
|
||||
});
|
||||
|
||||
this.options.auditLogger.accessAgreementAcknowledged(
|
||||
this.options.legacyAuditLogger.accessAgreementAcknowledged(
|
||||
currentUser.username,
|
||||
existingSessionValue.provider
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from '../../../../../src/core/server/mocks';
|
||||
import { licenseMock } from '../../common/licensing/index.mock';
|
||||
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
|
||||
import { securityAuditLoggerMock } from '../audit/index.mock';
|
||||
import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock';
|
||||
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
|
||||
import { sessionMock } from '../session_management/session.mock';
|
||||
|
||||
|
@ -42,13 +42,14 @@ import {
|
|||
InvalidateAPIKeyParams,
|
||||
} from './api_keys';
|
||||
import { SecurityLicense } from '../../common/licensing';
|
||||
import { SecurityAuditLogger } from '../audit';
|
||||
import { AuditServiceSetup, SecurityAuditLogger } from '../audit';
|
||||
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
|
||||
import { Session } from '../session_management';
|
||||
|
||||
describe('setupAuthentication()', () => {
|
||||
let mockSetupAuthenticationParams: {
|
||||
auditLogger: jest.Mocked<SecurityAuditLogger>;
|
||||
legacyAuditLogger: jest.Mocked<SecurityAuditLogger>;
|
||||
audit: jest.Mocked<AuditServiceSetup>;
|
||||
config: ConfigType;
|
||||
loggers: LoggerFactory;
|
||||
http: jest.Mocked<HttpServiceSetup>;
|
||||
|
@ -60,7 +61,8 @@ describe('setupAuthentication()', () => {
|
|||
let mockScopedClusterClient: jest.Mocked<PublicMethodsOf<LegacyScopedClusterClient>>;
|
||||
beforeEach(() => {
|
||||
mockSetupAuthenticationParams = {
|
||||
auditLogger: securityAuditLoggerMock.create(),
|
||||
legacyAuditLogger: securityAuditLoggerMock.create(),
|
||||
audit: auditServiceMock.create(),
|
||||
http: coreMock.createSetup().http,
|
||||
config: createConfig(
|
||||
ConfigSchema.validate({
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../../../src/core/server';
|
||||
import { SecurityLicense } from '../../common/licensing';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { SecurityAuditLogger } from '../audit';
|
||||
import { SecurityAuditLogger, AuditServiceSetup } from '../audit';
|
||||
import { ConfigType } from '../config';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
|
||||
|
@ -45,7 +45,8 @@ export {
|
|||
} from './http_authentication';
|
||||
|
||||
interface SetupAuthenticationParams {
|
||||
auditLogger: SecurityAuditLogger;
|
||||
legacyAuditLogger: SecurityAuditLogger;
|
||||
audit: AuditServiceSetup;
|
||||
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
|
||||
http: HttpServiceSetup;
|
||||
clusterClient: ILegacyClusterClient;
|
||||
|
@ -58,7 +59,8 @@ interface SetupAuthenticationParams {
|
|||
export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>;
|
||||
|
||||
export async function setupAuthentication({
|
||||
auditLogger,
|
||||
legacyAuditLogger: auditLogger,
|
||||
audit,
|
||||
getFeatureUsageService,
|
||||
http,
|
||||
clusterClient,
|
||||
|
@ -82,7 +84,8 @@ export async function setupAuthentication({
|
|||
};
|
||||
|
||||
const authenticator = new Authenticator({
|
||||
auditLogger,
|
||||
legacyAuditLogger: auditLogger,
|
||||
audit,
|
||||
loggers,
|
||||
clusterClient,
|
||||
basePath: http.basePath,
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('crypto', () => ({ randomBytes: jest.fn() }));
|
||||
jest.mock('crypto', () => ({
|
||||
randomBytes: jest.fn(),
|
||||
constants: jest.requireActual('crypto').constants,
|
||||
}));
|
||||
|
||||
import { loggingSystemMock } from '../../../../src/core/server/mocks';
|
||||
import { createConfig, ConfigSchema } from './config';
|
||||
|
@ -150,31 +153,23 @@ describe('config schema', () => {
|
|||
});
|
||||
|
||||
it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ encryptionKey: 'foo' })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."`
|
||||
expect(() => ConfigSchema.validate({ encryptionKey: 'foo' })).toThrow(
|
||||
'[encryptionKey]: value has length [3] but it must have a minimum length of [32].'
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."`
|
||||
expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true })).toThrow(
|
||||
'[encryptionKey]: value has length [3] but it must have a minimum length of [32].'
|
||||
);
|
||||
});
|
||||
|
||||
describe('authc.oidc', () => {
|
||||
it(`returns a validation error when authc.providers is "['oidc']" and realm is unspecified`, async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['oidc'] } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'] } })).toThrow(
|
||||
'[authc.oidc.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } })).toThrow(
|
||||
'[authc.oidc.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -204,10 +199,8 @@ describe('config schema', () => {
|
|||
});
|
||||
|
||||
it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.oidc.realm]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } })).toThrow(
|
||||
'[authc.oidc.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -240,22 +233,18 @@ describe('config schema', () => {
|
|||
it(`realm is not allowed when authc.providers is "['basic']"`, async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['basic'], oidc: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.oidc]: a value wasn't expected to be present"`);
|
||||
).toThrow("[authc.oidc]: a value wasn't expected to be present");
|
||||
});
|
||||
});
|
||||
|
||||
describe('authc.saml', () => {
|
||||
it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['saml'] } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.saml.realm]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => ConfigSchema.validate({ authc: { providers: ['saml'] } })).toThrow(
|
||||
'[authc.saml.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.saml.realm]: expected value of type [string] but got [undefined]"`
|
||||
expect(() => ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })).toThrow(
|
||||
'[authc.saml.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -285,7 +274,7 @@ describe('config schema', () => {
|
|||
it('`realm` is not allowed if saml provider is not enabled', async () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ authc: { providers: ['basic'], saml: { realm: 'realm-1' } } })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[authc.saml]: a value wasn't expected to be present"`);
|
||||
).toThrow("[authc.saml]: a value wasn't expected to be present");
|
||||
});
|
||||
|
||||
it('`maxRedirectURLSize` accepts any positive value that can coerce to `ByteSizeValue`', async () => {
|
||||
|
@ -360,11 +349,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { basic: { basic1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot be hidden from selector', () => {
|
||||
|
@ -374,11 +361,9 @@ describe('config schema', () => {
|
|||
providers: { basic: { basic1: { order: 0, showInSelector: false } } },
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.basic.basic1.showInSelector]: \`basic\` provider only supports \`true\` in \`showInSelector\`."
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.basic.basic1.showInSelector]: `basic` provider only supports `true` in `showInSelector`.'
|
||||
);
|
||||
});
|
||||
|
||||
it('can have only provider of this type', () => {
|
||||
|
@ -386,11 +371,7 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { basic: { basic1: { order: 0 }, basic2: { order: 1 } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.basic]: Only one \\"basic\\" provider can be configured."
|
||||
`);
|
||||
).toThrow('[authc.providers.1.basic]: Only one "basic" provider can be configured');
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -420,11 +401,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { token: { token1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot be hidden from selector', () => {
|
||||
|
@ -434,11 +413,9 @@ describe('config schema', () => {
|
|||
providers: { token: { token1: { order: 0, showInSelector: false } } },
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.token.token1.showInSelector]: \`token\` provider only supports \`true\` in \`showInSelector\`."
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.token.token1.showInSelector]: `token` provider only supports `true` in `showInSelector`.'
|
||||
);
|
||||
});
|
||||
|
||||
it('can have only provider of this type', () => {
|
||||
|
@ -446,11 +423,7 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { token: { token1: { order: 0 }, token2: { order: 1 } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.token]: Only one \\"token\\" provider can be configured."
|
||||
`);
|
||||
).toThrow('[authc.providers.1.token]: Only one "token" provider can be configured');
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -480,11 +453,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { pki: { pki1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('can have only provider of this type', () => {
|
||||
|
@ -492,11 +463,7 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { pki: { pki1: { order: 0 }, pki2: { order: 1 } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.pki]: Only one \\"pki\\" provider can be configured."
|
||||
`);
|
||||
).toThrow('[authc.providers.1.pki]: Only one "pki" provider can be configured');
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -524,11 +491,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { kerberos: { kerberos1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('can have only provider of this type', () => {
|
||||
|
@ -538,11 +503,7 @@ describe('config schema', () => {
|
|||
providers: { kerberos: { kerberos1: { order: 0 }, kerberos2: { order: 1 } } },
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.kerberos]: Only one \\"kerberos\\" provider can be configured."
|
||||
`);
|
||||
).toThrow('[authc.providers.1.kerberos]: Only one "kerberos" provider can be configured');
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -570,11 +531,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { oidc: { oidc1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('requires `realm`', () => {
|
||||
|
@ -582,11 +541,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { oidc: { oidc1: { order: 0 } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -625,11 +582,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { saml: { saml1: { enabled: true } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('requires `realm`', () => {
|
||||
|
@ -637,11 +592,9 @@ describe('config schema', () => {
|
|||
ConfigSchema.validate({
|
||||
authc: { providers: { saml: { saml1: { order: 0 } } } },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be successfully validated', () => {
|
||||
|
@ -703,11 +656,9 @@ describe('config schema', () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1]: Found multiple providers configured with the same name \\"provider1\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1]: Found multiple providers configured with the same name "provider1": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]'
|
||||
);
|
||||
});
|
||||
|
||||
it('`order` should be unique across all provider types', () => {
|
||||
|
@ -723,11 +674,9 @@ describe('config schema', () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[authc.providers]: types that failed validation:
|
||||
- [authc.providers.0]: expected value of type [array] but got [Object]
|
||||
- [authc.providers.1]: Found multiple providers configured with the same order \\"0\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]"
|
||||
`);
|
||||
).toThrow(
|
||||
'[authc.providers.1]: Found multiple providers configured with the same order "0": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be successfully validated with multiple providers ignoring uniqueness violations in disabled ones', () => {
|
||||
|
@ -792,10 +741,8 @@ describe('config schema', () => {
|
|||
|
||||
describe('session', () => {
|
||||
it('should throw error if xpack.security.session.cleanupInterval is less than 10 seconds', () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({ session: { cleanupInterval: '9s' } })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[session.cleanupInterval]: the value must be greater or equal to 10 seconds."`
|
||||
expect(() => ConfigSchema.validate({ session: { cleanupInterval: '9s' } })).toThrow(
|
||||
'[session.cleanupInterval]: the value must be greater or equal to 10 seconds.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1091,4 +1038,55 @@ describe('createConfig()', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('accepts an audit appender', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
audit: {
|
||||
appender: {
|
||||
kind: 'file',
|
||||
path: '/path/to/file.txt',
|
||||
layout: {
|
||||
kind: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
}).audit.appender
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"kind": "file",
|
||||
"layout": Object {
|
||||
"kind": "json",
|
||||
},
|
||||
"path": "/path/to/file.txt",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects an appender if not fully configured', () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({
|
||||
audit: {
|
||||
// no layout configured
|
||||
appender: {
|
||||
kind: 'file',
|
||||
path: '/path/to/file.txt',
|
||||
},
|
||||
},
|
||||
})
|
||||
).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]');
|
||||
});
|
||||
|
||||
it('rejects an ignore_filter when no appender is configured', () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate({
|
||||
audit: {
|
||||
enabled: true,
|
||||
ignore_filters: [{ actions: ['some_action'] }],
|
||||
},
|
||||
})
|
||||
).toThrow(
|
||||
'[audit]: xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import crypto from 'crypto';
|
||||
import { schema, Type, TypeOf } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Logger } from '../../../../src/core/server';
|
||||
import { Logger, config as coreConfig } from '../../../../src/core/server';
|
||||
|
||||
export type ConfigType = ReturnType<typeof createConfig>;
|
||||
|
||||
|
@ -198,9 +198,30 @@ export const ConfigSchema = schema.object({
|
|||
schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }),
|
||||
}),
|
||||
}),
|
||||
audit: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
audit: schema.object(
|
||||
{
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
appender: schema.maybe(coreConfig.logging.appenders),
|
||||
ignore_filters: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
actions: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
categories: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
types: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
outcomes: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
spaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
validate: (auditConfig) => {
|
||||
if (auditConfig.ignore_filters && !auditConfig.appender) {
|
||||
return 'xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.';
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export function createConfig(
|
||||
|
|
|
@ -27,7 +27,7 @@ export {
|
|||
SAMLLogin,
|
||||
OIDCLogin,
|
||||
} from './authentication';
|
||||
export { AuditLogger } from './audit';
|
||||
export { LegacyAuditLogger } from './audit';
|
||||
export { SecurityPluginSetup };
|
||||
export { AuthenticatedUser } from '../common/model';
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ describe('Security Plugin', () => {
|
|||
await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"audit": Object {
|
||||
"asScoped": [Function],
|
||||
"getLogger": [Function],
|
||||
},
|
||||
"authc": Object {
|
||||
|
|
|
@ -67,7 +67,7 @@ export interface SecurityPluginSetup {
|
|||
'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode'
|
||||
>;
|
||||
license: SecurityLicense;
|
||||
audit: Pick<AuditServiceSetup, 'getLogger'>;
|
||||
audit: AuditServiceSetup;
|
||||
|
||||
/**
|
||||
* If Spaces plugin is available it's supposed to register its SpacesService with Security plugin
|
||||
|
@ -101,6 +101,7 @@ export class Plugin {
|
|||
private readonly logger: Logger;
|
||||
private spacesService?: SpacesService | symbol = Symbol('not accessed');
|
||||
private securityLicenseService?: SecurityLicenseService;
|
||||
private authc?: Authentication;
|
||||
|
||||
private readonly featureUsageService = new SecurityFeatureUsageService();
|
||||
private featureUsageServiceStart?: SecurityFeatureUsageServiceStart;
|
||||
|
@ -176,8 +177,15 @@ export class Plugin {
|
|||
|
||||
registerSecurityUsageCollector({ usageCollection, config, license });
|
||||
|
||||
const audit = this.auditService.setup({ license, config: config.audit });
|
||||
const auditLogger = new SecurityAuditLogger(audit.getLogger());
|
||||
const audit = this.auditService.setup({
|
||||
license,
|
||||
config: config.audit,
|
||||
logging: core.logging,
|
||||
http: core.http,
|
||||
getSpaceId: (request) => this.getSpacesService()?.getSpaceId(request),
|
||||
getCurrentUser: (request) => this.authc?.getCurrentUser(request),
|
||||
});
|
||||
const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger());
|
||||
|
||||
const { session } = this.sessionManagementService.setup({
|
||||
config,
|
||||
|
@ -187,8 +195,9 @@ export class Plugin {
|
|||
taskManager,
|
||||
});
|
||||
|
||||
const authc = await setupAuthentication({
|
||||
auditLogger,
|
||||
this.authc = await setupAuthentication({
|
||||
legacyAuditLogger,
|
||||
audit,
|
||||
getFeatureUsageService: this.getFeatureUsageService,
|
||||
http: core.http,
|
||||
clusterClient,
|
||||
|
@ -209,11 +218,12 @@ export class Plugin {
|
|||
buildNumber: this.initializerContext.env.packageInfo.buildNum,
|
||||
getSpacesService: this.getSpacesService,
|
||||
features,
|
||||
getCurrentUser: authc.getCurrentUser,
|
||||
getCurrentUser: this.authc.getCurrentUser,
|
||||
});
|
||||
|
||||
setupSavedObjects({
|
||||
auditLogger,
|
||||
legacyAuditLogger,
|
||||
audit,
|
||||
authz,
|
||||
savedObjects: core.savedObjects,
|
||||
getSpacesService: this.getSpacesService,
|
||||
|
@ -226,7 +236,7 @@ export class Plugin {
|
|||
logger: this.initializerContext.logger.get('routes'),
|
||||
clusterClient,
|
||||
config,
|
||||
authc,
|
||||
authc: this.authc,
|
||||
authz,
|
||||
license,
|
||||
session,
|
||||
|
@ -239,17 +249,18 @@ export class Plugin {
|
|||
|
||||
return deepFreeze<SecurityPluginSetup>({
|
||||
audit: {
|
||||
asScoped: audit.asScoped,
|
||||
getLogger: audit.getLogger,
|
||||
},
|
||||
|
||||
authc: {
|
||||
isAuthenticated: authc.isAuthenticated,
|
||||
getCurrentUser: authc.getCurrentUser,
|
||||
areAPIKeysEnabled: authc.areAPIKeysEnabled,
|
||||
createAPIKey: authc.createAPIKey,
|
||||
invalidateAPIKey: authc.invalidateAPIKey,
|
||||
grantAPIKeyAsInternalUser: authc.grantAPIKeyAsInternalUser,
|
||||
invalidateAPIKeyAsInternalUser: authc.invalidateAPIKeyAsInternalUser,
|
||||
isAuthenticated: this.authc.isAuthenticated,
|
||||
getCurrentUser: this.authc.getCurrentUser,
|
||||
areAPIKeysEnabled: this.authc.areAPIKeysEnabled,
|
||||
createAPIKey: this.authc.createAPIKey,
|
||||
invalidateAPIKey: this.authc.invalidateAPIKey,
|
||||
grantAPIKeyAsInternalUser: this.authc.grantAPIKeyAsInternalUser,
|
||||
invalidateAPIKeyAsInternalUser: this.authc.invalidateAPIKeyAsInternalUser,
|
||||
},
|
||||
|
||||
authz: {
|
||||
|
|
|
@ -12,11 +12,12 @@ import {
|
|||
} from '../../../../../src/core/server';
|
||||
import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
|
||||
import { AuthorizationServiceSetup } from '../authorization';
|
||||
import { SecurityAuditLogger } from '../audit';
|
||||
import { SecurityAuditLogger, AuditServiceSetup } from '../audit';
|
||||
import { SpacesService } from '../plugin';
|
||||
|
||||
interface SetupSavedObjectsParams {
|
||||
auditLogger: SecurityAuditLogger;
|
||||
legacyAuditLogger: SecurityAuditLogger;
|
||||
audit: AuditServiceSetup;
|
||||
authz: Pick<
|
||||
AuthorizationServiceSetup,
|
||||
'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest'
|
||||
|
@ -26,7 +27,8 @@ interface SetupSavedObjectsParams {
|
|||
}
|
||||
|
||||
export function setupSavedObjects({
|
||||
auditLogger,
|
||||
legacyAuditLogger,
|
||||
audit,
|
||||
authz,
|
||||
savedObjects,
|
||||
getSpacesService,
|
||||
|
@ -50,7 +52,8 @@ export function setupSavedObjects({
|
|||
return authz.mode.useRbacForRequest(kibanaRequest)
|
||||
? new SecureSavedObjectsClientWrapper({
|
||||
actions: authz.actions,
|
||||
auditLogger,
|
||||
legacyAuditLogger,
|
||||
auditLogger: audit.asScoped(kibanaRequest),
|
||||
baseClient: client,
|
||||
checkSavedObjectsPrivilegesAsCurrentUser: authz.checkSavedObjectsPrivilegesWithRequest(
|
||||
kibanaRequest
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
|
||||
import { Actions } from '../authorization';
|
||||
import { securityAuditLoggerMock } from '../audit/index.mock';
|
||||
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { securityAuditLoggerMock, auditServiceMock } from '../audit/index.mock';
|
||||
import { savedObjectsClientMock, httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import { SavedObjectActions } from '../authorization/actions/saved_object';
|
||||
import { AuditEvent, EventOutcome } from '../audit';
|
||||
|
||||
let clientOpts: ReturnType<typeof createSecureSavedObjectsClientWrapperOptions>;
|
||||
let client: SecureSavedObjectsClientWrapper;
|
||||
|
@ -38,7 +39,8 @@ const createSecureSavedObjectsClientWrapperOptions = () => {
|
|||
checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(),
|
||||
errors,
|
||||
getSpacesService,
|
||||
auditLogger: securityAuditLoggerMock.create(),
|
||||
legacyAuditLogger: securityAuditLoggerMock.create(),
|
||||
auditLogger: auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()),
|
||||
forbiddenError,
|
||||
generalError,
|
||||
};
|
||||
|
@ -53,8 +55,8 @@ const expectGeneralError = async (fn: Function, args: Record<string, any>) => {
|
|||
clientOpts.generalError
|
||||
);
|
||||
expect(clientOpts.errors.decorateGeneralError).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -84,8 +86,8 @@ const expectForbiddenError = async (fn: Function, args: Record<string, any>, act
|
|||
const spaceIds = [spaceId];
|
||||
|
||||
expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
action ?? ACTION,
|
||||
types,
|
||||
|
@ -93,7 +95,7 @@ const expectForbiddenError = async (fn: Function, args: Record<string, any>, act
|
|||
missing,
|
||||
args
|
||||
);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const expectSuccess = async (fn: Function, args: Record<string, any>, action?: string) => {
|
||||
|
@ -105,9 +107,9 @@ const expectSuccess = async (fn: Function, args: Record<string, any>, action?: s
|
|||
const types = getCalls.map((x) => x[0]);
|
||||
const spaceIds = args.options?.namespaces || [args.options?.namespace || 'default'];
|
||||
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
action ?? ACTION,
|
||||
types,
|
||||
|
@ -176,6 +178,26 @@ const expectObjectNamespaceFiltering = async (
|
|||
);
|
||||
};
|
||||
|
||||
const expectAuditEvent = (
|
||||
action: AuditEvent['event']['action'],
|
||||
outcome: AuditEvent['event']['outcome'],
|
||||
savedObject?: Required<AuditEvent>['kibana']['saved_object']
|
||||
) => {
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action,
|
||||
outcome,
|
||||
}),
|
||||
kibana: savedObject
|
||||
? expect.objectContaining({
|
||||
saved_object: savedObject,
|
||||
})
|
||||
: expect.anything(),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const expectObjectsNamespaceFiltering = async (fn: Function, args: Record<string, any>) => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce(
|
||||
getMockCheckPrivilegesSuccess // privilege check for authorization
|
||||
|
@ -200,15 +222,13 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record<string
|
|||
clientOpts.baseClient.find.mockReturnValue(returnValue as any);
|
||||
|
||||
const result = await fn.bind(client)(...Object.values(args));
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
saved_objects: [
|
||||
{ namespaces: ['?'] },
|
||||
{ namespaces: [authorizedNamespace] },
|
||||
{ namespaces: [authorizedNamespace, '?'] },
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({
|
||||
saved_objects: [
|
||||
{ namespaces: ['?'] },
|
||||
{ namespaces: [authorizedNamespace] },
|
||||
{ namespaces: [authorizedNamespace, '?'] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2);
|
||||
expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith('login:', [
|
||||
|
@ -299,8 +319,10 @@ describe('#addToNamespaces', () => {
|
|||
);
|
||||
|
||||
expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure
|
||||
).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
'addToNamespacesCreate',
|
||||
[type],
|
||||
|
@ -308,7 +330,7 @@ describe('#addToNamespaces', () => {
|
|||
[{ privilege, spaceId: newNs1 }],
|
||||
{ id, type, namespaces, options: {} }
|
||||
);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => {
|
||||
|
@ -324,9 +346,9 @@ describe('#addToNamespaces', () => {
|
|||
);
|
||||
|
||||
expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
clientOpts.auditLogger.savedObjectsAuthorizationFailure
|
||||
clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure
|
||||
).toHaveBeenLastCalledWith(
|
||||
USERNAME,
|
||||
'addToNamespacesUpdate',
|
||||
|
@ -335,7 +357,7 @@ describe('#addToNamespaces', () => {
|
|||
[{ privilege, spaceId: currentNs }],
|
||||
{ id, type, namespaces, options: {} }
|
||||
);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test(`returns result of baseClient.addToNamespaces when authorized`, async () => {
|
||||
|
@ -345,9 +367,9 @@ describe('#addToNamespaces', () => {
|
|||
const result = await client.addToNamespaces(type, id, namespaces);
|
||||
expect(result).toBe(apiCallReturnValue);
|
||||
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
USERNAME,
|
||||
'addToNamespacesCreate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesCreate'
|
||||
|
@ -355,7 +377,7 @@ describe('#addToNamespaces', () => {
|
|||
namespaces.sort(),
|
||||
{ type, id, namespaces, options: {} }
|
||||
);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
USERNAME,
|
||||
'addToNamespacesUpdate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesUpdate'
|
||||
|
@ -392,12 +414,28 @@ describe('#addToNamespaces', () => {
|
|||
// this operation is unique because it requires two privilege checks before it executes
|
||||
await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2);
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any);
|
||||
await client.addToNamespaces(type, id, namespaces);
|
||||
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_add_to_spaces', EventOutcome.UNKNOWN, { type, id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.addToNamespaces(type, id, namespaces)).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_add_to_spaces', EventOutcome.FAILURE, { type, id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkCreate', () => {
|
||||
const attributes = { some: 'attr' };
|
||||
const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup', attributes });
|
||||
const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone', attributes });
|
||||
const obj1 = Object.freeze({ type: 'foo', id: 'sup', attributes });
|
||||
const obj2 = Object.freeze({ type: 'bar', id: 'everyone', attributes });
|
||||
const namespace = 'some-ns';
|
||||
|
||||
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
|
||||
|
@ -445,6 +483,25 @@ describe('#bulkCreate', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectsNamespaceFiltering(client.bulkCreate, { objects, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = { saved_objects: [], foo: 'bar' };
|
||||
clientOpts.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any);
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.bulkCreate, { objects, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.bulkCreate([obj1, obj2], { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkGet', () => {
|
||||
|
@ -484,6 +541,25 @@ describe('#bulkGet', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectsNamespaceFiltering(client.bulkGet, { objects, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = { saved_objects: [], foo: 'bar' };
|
||||
clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any);
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.bulkGet, { objects, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj1);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj2);
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.bulkGet([obj1, obj2], { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj1);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkUpdate', () => {
|
||||
|
@ -534,6 +610,25 @@ describe('#bulkUpdate', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectsNamespaceFiltering(client.bulkUpdate, { objects, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = { saved_objects: [], foo: 'bar' };
|
||||
clientOpts.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any);
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.bulkUpdate, { objects, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.bulkUpdate<any>([obj1, obj2], { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkConflicts', () => {
|
||||
|
@ -614,6 +709,22 @@ describe('#create', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectNamespaceFiltering(client.create, { type, attributes, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any);
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.create, { type, attributes, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.create(type, attributes, { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
|
@ -643,6 +754,22 @@ describe('#delete', () => {
|
|||
const options = { namespace };
|
||||
await expectPrivilegeCheck(client.delete, { type, id, options }, namespace);
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.delete.mockReturnValue(apiCallReturnValue as any);
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.delete, { type, id, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_delete', EventOutcome.UNKNOWN, { type, id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.delete(type, id)).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_delete', EventOutcome.FAILURE, { type, id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
|
@ -663,8 +790,10 @@ describe('#find', () => {
|
|||
const result = await client.find(options);
|
||||
|
||||
expect(clientOpts.baseClient.find).not.toHaveBeenCalled();
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure
|
||||
).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
'find',
|
||||
[type1],
|
||||
|
@ -759,6 +888,27 @@ describe('#find', () => {
|
|||
const options = { type: [type1, type2], namespaces };
|
||||
await expectObjectsNamespaceFiltering(client.find, { options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const obj1 = { type: 'foo', id: 'sup' };
|
||||
const obj2 = { type: 'bar', id: 'everyone' };
|
||||
const apiCallReturnValue = { saved_objects: [obj1, obj2], foo: 'bar' };
|
||||
clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any);
|
||||
const options = Object.freeze({ type: type1, namespaces: ['some-ns'] });
|
||||
await expectSuccess(client.find, { options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj1);
|
||||
expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj2);
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation(
|
||||
getMockCheckPrivilegesFailure
|
||||
);
|
||||
await client.find({ type: type1 });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_find', EventOutcome.FAILURE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
|
@ -793,6 +943,22 @@ describe('#get', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectNamespaceFiltering(client.get, { type, id, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.get.mockReturnValue(apiCallReturnValue as any);
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.get, { type, id, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, { type, id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.get(type, id, { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_get', EventOutcome.FAILURE, { type, id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteFromNamespaces', () => {
|
||||
|
@ -817,8 +983,8 @@ describe('#deleteFromNamespaces', () => {
|
|||
);
|
||||
|
||||
expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces'
|
||||
[type],
|
||||
|
@ -826,7 +992,7 @@ describe('#deleteFromNamespaces', () => {
|
|||
[{ privilege, spaceId: namespace1 }],
|
||||
{ type, id, namespaces, options: {} }
|
||||
);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => {
|
||||
|
@ -836,9 +1002,9 @@ describe('#deleteFromNamespaces', () => {
|
|||
const result = await client.deleteFromNamespaces(type, id, namespaces);
|
||||
expect(result).toBe(apiCallReturnValue);
|
||||
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
USERNAME,
|
||||
'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces'
|
||||
[type],
|
||||
|
@ -864,6 +1030,21 @@ describe('#deleteFromNamespaces', () => {
|
|||
test(`filters namespaces that the user doesn't have access to`, async () => {
|
||||
await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any);
|
||||
await client.deleteFromNamespaces(type, id, namespaces);
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.UNKNOWN, { type, id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.FAILURE, { type, id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
|
@ -899,6 +1080,22 @@ describe('#update', () => {
|
|||
const options = { namespace };
|
||||
await expectObjectNamespaceFiltering(client.update, { type, id, attributes, options });
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = Symbol();
|
||||
clientOpts.baseClient.update.mockReturnValue(apiCallReturnValue as any);
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.update, { type, id, attributes, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type, id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.update(type, id, attributes, { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1);
|
||||
expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type, id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('other', () => {
|
||||
|
|
|
@ -23,10 +23,12 @@ import { SecurityAuditLogger } from '../audit';
|
|||
import { Actions, CheckSavedObjectsPrivileges } from '../authorization';
|
||||
import { CheckPrivilegesResponse } from '../authorization/types';
|
||||
import { SpacesService } from '../plugin';
|
||||
import { AuditLogger, EventOutcome, SavedObjectAction, savedObjectEvent } from '../audit';
|
||||
|
||||
interface SecureSavedObjectsClientWrapperOptions {
|
||||
actions: Actions;
|
||||
auditLogger: SecurityAuditLogger;
|
||||
legacyAuditLogger: SecurityAuditLogger;
|
||||
auditLogger: AuditLogger;
|
||||
baseClient: SavedObjectsClientContract;
|
||||
errors: SavedObjectsClientContract['errors'];
|
||||
checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
|
||||
|
@ -58,7 +60,8 @@ interface EnsureAuthorizedTypeResult {
|
|||
|
||||
export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract {
|
||||
private readonly actions: Actions;
|
||||
private readonly auditLogger: PublicMethodsOf<SecurityAuditLogger>;
|
||||
private readonly legacyAuditLogger: PublicMethodsOf<SecurityAuditLogger>;
|
||||
private readonly auditLogger: AuditLogger;
|
||||
private readonly baseClient: SavedObjectsClientContract;
|
||||
private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
|
||||
private getSpacesService: () => SpacesService | undefined;
|
||||
|
@ -66,6 +69,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
|
||||
constructor({
|
||||
actions,
|
||||
legacyAuditLogger,
|
||||
auditLogger,
|
||||
baseClient,
|
||||
checkSavedObjectsPrivilegesAsCurrentUser,
|
||||
|
@ -74,6 +78,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
}: SecureSavedObjectsClientWrapperOptions) {
|
||||
this.errors = errors;
|
||||
this.actions = actions;
|
||||
this.legacyAuditLogger = legacyAuditLogger;
|
||||
this.auditLogger = auditLogger;
|
||||
this.baseClient = baseClient;
|
||||
this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser;
|
||||
|
@ -85,9 +90,27 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
attributes: T = {} as T,
|
||||
options: SavedObjectsCreateOptions = {}
|
||||
) {
|
||||
const args = { type, attributes, options };
|
||||
const namespaces = [options.namespace, ...(options.initialNamespaces || [])];
|
||||
await this.ensureAuthorized(type, 'create', namespaces, { args });
|
||||
try {
|
||||
const args = { type, attributes, options };
|
||||
const namespaces = [options.namespace, ...(options.initialNamespaces || [])];
|
||||
await this.ensureAuthorized(type, 'create', namespaces, { args });
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
savedObject: { type, id: options.id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id: options.id },
|
||||
})
|
||||
);
|
||||
|
||||
const savedObject = await this.baseClient.create(type, attributes, options);
|
||||
return await this.redactSavedObjectNamespaces(savedObject);
|
||||
|
@ -112,25 +135,65 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
objects: Array<SavedObjectsBulkCreateObject<T>>,
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
) {
|
||||
const args = { objects, options };
|
||||
const namespaces = objects.reduce(
|
||||
(acc, { initialNamespaces = [] }) => {
|
||||
return acc.concat(initialNamespaces);
|
||||
},
|
||||
[options.namespace]
|
||||
);
|
||||
try {
|
||||
const args = { objects, options };
|
||||
const namespaces = objects.reduce(
|
||||
(acc, { initialNamespaces = [] }) => {
|
||||
return acc.concat(initialNamespaces);
|
||||
},
|
||||
[options.namespace]
|
||||
);
|
||||
|
||||
await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, {
|
||||
args,
|
||||
});
|
||||
await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, {
|
||||
args,
|
||||
});
|
||||
} catch (error) {
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.CREATE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const response = await this.baseClient.bulkCreate(objects, options);
|
||||
return await this.redactSavedObjectsNamespaces(response);
|
||||
}
|
||||
|
||||
public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
|
||||
const args = { type, id, options };
|
||||
await this.ensureAuthorized(type, 'delete', options.namespace, { args });
|
||||
try {
|
||||
const args = { type, id, options };
|
||||
await this.ensureAuthorized(type, 'delete', options.namespace, { args });
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
);
|
||||
|
||||
return await this.baseClient.delete(type, id, options);
|
||||
}
|
||||
|
@ -145,6 +208,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
`_find across namespaces is not permitted when the Spaces plugin is disabled.`
|
||||
);
|
||||
}
|
||||
|
||||
const args = { options };
|
||||
const { status, typeMap } = await this.ensureAuthorized(
|
||||
options.type,
|
||||
|
@ -155,6 +219,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
|
||||
if (status === 'unauthorized') {
|
||||
// return empty response
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.FIND,
|
||||
error: new Error(status),
|
||||
})
|
||||
);
|
||||
return SavedObjectsUtils.createEmptyFindResponse<T>(options);
|
||||
}
|
||||
|
||||
|
@ -163,11 +233,22 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
isGloballyAuthorized ? acc.set(type, options.namespaces) : acc.set(type, authorizedSpaces),
|
||||
new Map()
|
||||
);
|
||||
|
||||
const response = await this.baseClient.find<T>({
|
||||
...options,
|
||||
typeToNamespacesMap: undefined, // if the user is fully authorized, use `undefined` as the typeToNamespacesMap to prevent privilege escalation
|
||||
...(status === 'partially_authorized' && { typeToNamespacesMap, type: '', namespaces: [] }), // the repository requires that `type` and `namespaces` must be empty if `typeToNamespacesMap` is defined
|
||||
});
|
||||
|
||||
response.saved_objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.FIND,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return await this.redactSavedObjectsNamespaces(response);
|
||||
}
|
||||
|
||||
|
@ -175,20 +256,67 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
objects: SavedObjectsBulkGetObject[] = [],
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
) {
|
||||
const args = { objects, options };
|
||||
await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, {
|
||||
args,
|
||||
});
|
||||
try {
|
||||
const args = { objects, options };
|
||||
await this.ensureAuthorized(
|
||||
this.getUniqueObjectTypes(objects),
|
||||
'bulk_get',
|
||||
options.namespace,
|
||||
{
|
||||
args,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.GET,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response = await this.baseClient.bulkGet<T>(objects, options);
|
||||
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.GET,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return await this.redactSavedObjectsNamespaces(response);
|
||||
}
|
||||
|
||||
public async get<T = unknown>(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
|
||||
const args = { type, id, options };
|
||||
await this.ensureAuthorized(type, 'get', options.namespace, { args });
|
||||
try {
|
||||
const args = { type, id, options };
|
||||
await this.ensureAuthorized(type, 'get', options.namespace, { args });
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.GET,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const savedObject = await this.baseClient.get<T>(type, id, options);
|
||||
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.GET,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
);
|
||||
|
||||
return await this.redactSavedObjectNamespaces(savedObject);
|
||||
}
|
||||
|
||||
|
@ -198,8 +326,26 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
attributes: Partial<T>,
|
||||
options: SavedObjectsUpdateOptions = {}
|
||||
) {
|
||||
const args = { type, id, attributes, options };
|
||||
await this.ensureAuthorized(type, 'update', options.namespace, { args });
|
||||
try {
|
||||
const args = { type, id, attributes, options };
|
||||
await this.ensureAuthorized(type, 'update', options.namespace, { args });
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.UPDATE,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.UPDATE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
);
|
||||
|
||||
const savedObject = await this.baseClient.update(type, id, attributes, options);
|
||||
return await this.redactSavedObjectNamespaces(savedObject);
|
||||
|
@ -211,25 +357,45 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
namespaces: string[],
|
||||
options: SavedObjectsAddToNamespacesOptions = {}
|
||||
) {
|
||||
const args = { type, id, namespaces, options };
|
||||
const { namespace } = options;
|
||||
// To share an object, the user must have the "share_to_space" permission in each of the destination namespaces.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespaces, {
|
||||
args,
|
||||
auditAction: 'addToNamespacesCreate',
|
||||
});
|
||||
try {
|
||||
const args = { type, id, namespaces, options };
|
||||
const { namespace } = options;
|
||||
// To share an object, the user must have the "share_to_space" permission in each of the destination namespaces.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespaces, {
|
||||
args,
|
||||
auditAction: 'addToNamespacesCreate',
|
||||
});
|
||||
|
||||
// To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the
|
||||
// `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in
|
||||
// the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation
|
||||
// will result in a 404 error.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespace, {
|
||||
args,
|
||||
auditAction: 'addToNamespacesUpdate',
|
||||
});
|
||||
// To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the
|
||||
// `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in
|
||||
// the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation
|
||||
// will result in a 404 error.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespace, {
|
||||
args,
|
||||
auditAction: 'addToNamespacesUpdate',
|
||||
});
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.ADD_TO_SPACES,
|
||||
savedObject: { type, id },
|
||||
addToSpaces: namespaces,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.ADD_TO_SPACES,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
addToSpaces: namespaces,
|
||||
})
|
||||
);
|
||||
|
||||
const result = await this.baseClient.addToNamespaces(type, id, namespaces, options);
|
||||
return await this.redactSavedObjectNamespaces(result);
|
||||
const response = await this.baseClient.addToNamespaces(type, id, namespaces, options);
|
||||
return await this.redactSavedObjectNamespaces(response);
|
||||
}
|
||||
|
||||
public async deleteFromNamespaces(
|
||||
|
@ -238,31 +404,73 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
namespaces: string[],
|
||||
options: SavedObjectsDeleteFromNamespacesOptions = {}
|
||||
) {
|
||||
const args = { type, id, namespaces, options };
|
||||
// To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespaces, {
|
||||
args,
|
||||
auditAction: 'deleteFromNamespaces',
|
||||
});
|
||||
try {
|
||||
const args = { type, id, namespaces, options };
|
||||
// To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces.
|
||||
await this.ensureAuthorized(type, 'share_to_space', namespaces, {
|
||||
args,
|
||||
auditAction: 'deleteFromNamespaces',
|
||||
});
|
||||
} catch (error) {
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE_FROM_SPACES,
|
||||
savedObject: { type, id },
|
||||
deleteFromSpaces: namespaces,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE_FROM_SPACES,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
deleteFromSpaces: namespaces,
|
||||
})
|
||||
);
|
||||
|
||||
const result = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options);
|
||||
return await this.redactSavedObjectNamespaces(result);
|
||||
const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options);
|
||||
return await this.redactSavedObjectNamespaces(response);
|
||||
}
|
||||
|
||||
public async bulkUpdate<T = unknown>(
|
||||
objects: Array<SavedObjectsBulkUpdateObject<T>> = [],
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
) {
|
||||
const objectNamespaces = objects
|
||||
// The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace;
|
||||
// in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so.
|
||||
.filter(({ namespace }) => namespace !== undefined)
|
||||
.map(({ namespace }) => namespace!);
|
||||
const namespaces = [options?.namespace, ...objectNamespaces];
|
||||
const args = { objects, options };
|
||||
await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, {
|
||||
args,
|
||||
});
|
||||
try {
|
||||
const objectNamespaces = objects
|
||||
// The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace;
|
||||
// in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so.
|
||||
.filter(({ namespace }) => namespace !== undefined)
|
||||
.map(({ namespace }) => namespace!);
|
||||
const namespaces = [options?.namespace, ...objectNamespaces];
|
||||
const args = { objects, options };
|
||||
await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, {
|
||||
args,
|
||||
});
|
||||
} catch (error) {
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.UPDATE,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.UPDATE,
|
||||
outcome: EventOutcome.UNKNOWN,
|
||||
savedObject: { type, id },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const response = await this.baseClient.bulkUpdate<T>(objects, options);
|
||||
return await this.redactSavedObjectsNamespaces(response);
|
||||
|
@ -316,7 +524,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
);
|
||||
|
||||
const logAuthorizationFailure = () => {
|
||||
this.auditLogger.savedObjectsAuthorizationFailure(
|
||||
this.legacyAuditLogger.savedObjectsAuthorizationFailure(
|
||||
username,
|
||||
auditAction,
|
||||
types,
|
||||
|
@ -326,7 +534,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
);
|
||||
};
|
||||
const logAuthorizationSuccess = (typeArray: string[], spaceIdArray: string[]) => {
|
||||
this.auditLogger.savedObjectsAuthorizationSuccess(
|
||||
this.legacyAuditLogger.savedObjectsAuthorizationSuccess(
|
||||
username,
|
||||
auditAction,
|
||||
typeArray,
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import { LegacyAuditLogger } from '../../../security/server';
|
||||
|
||||
export class SpacesAuditLogger {
|
||||
private readonly auditLogger: AuditLogger;
|
||||
private readonly auditLogger: LegacyAuditLogger;
|
||||
|
||||
constructor(auditLogger: AuditLogger = { log() {} }) {
|
||||
constructor(auditLogger: LegacyAuditLogger = { log() {} }) {
|
||||
this.auditLogger = auditLogger;
|
||||
}
|
||||
public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) {
|
||||
|
|
|
@ -35,6 +35,7 @@ const onlyNotInCoverageTests = [
|
|||
require.resolve('../test/security_api_integration/session_idle.config.ts'),
|
||||
require.resolve('../test/security_api_integration/session_lifespan.config.ts'),
|
||||
require.resolve('../test/security_api_integration/login_selector.config.ts'),
|
||||
require.resolve('../test/security_api_integration/audit.config.ts'),
|
||||
require.resolve('../test/token_api_integration/config.js'),
|
||||
require.resolve('../test/oidc_api_integration/config.ts'),
|
||||
require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'),
|
||||
|
|
|
@ -27,7 +27,6 @@ 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,12 +49,6 @@ 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'),
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/*debug.log
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* 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() {}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
}
|
37
x-pack/test/security_api_integration/audit.config.ts
Normal file
37
x-pack/test/security_api_integration/audit.config.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
|
||||
|
||||
const auditLogPlugin = resolve(__dirname, './fixtures/audit/audit_log');
|
||||
const auditLogPath = resolve(__dirname, './fixtures/audit/audit.log');
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./tests/audit')],
|
||||
servers: xPackAPITestsConfig.get('servers'),
|
||||
security: { disableTestUser: true },
|
||||
services: xPackAPITestsConfig.get('services'),
|
||||
junit: {
|
||||
reportName: 'X-Pack Security API Integration Tests (Audit Log)',
|
||||
},
|
||||
esTestCluster: xPackAPITestsConfig.get('esTestCluster'),
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
`--plugin-path=${auditLogPlugin}`,
|
||||
'--xpack.security.audit.enabled=true',
|
||||
'--xpack.security.audit.appender.kind=file',
|
||||
`--xpack.security.audit.appender.path=${auditLogPath}`,
|
||||
'--xpack.security.audit.appender.layout.kind=json',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "audit_trail_test",
|
||||
"id": "auditLog",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": [],
|
||||
"requiredPlugins": ["auditTrail"],
|
||||
"requiredPlugins": [],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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) {
|
||||
const router = core.http.createRouter();
|
||||
router.get({ path: '/audit_log', validate: false }, async (context, request, response) => {
|
||||
await context.core.savedObjects.client.create('dashboard', {});
|
||||
await context.core.savedObjects.client.find({ type: 'dashboard' });
|
||||
return response.noContent();
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
}
|
118
x-pack/test/security_api_integration/tests/audit/audit_log.ts
Normal file
118
x-pack/test/security_api_integration/tests/audit/audit_log.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const { username, password } = getService('config').get('servers.kibana');
|
||||
|
||||
describe('Audit Log', function () {
|
||||
const logFilePath = Path.resolve(__dirname, '../../fixtures/audit/audit.log');
|
||||
const logFile = new FileWrapper(logFilePath);
|
||||
|
||||
beforeEach(async () => {
|
||||
await logFile.reset();
|
||||
});
|
||||
|
||||
it('logs audit events when reading and writing saved objects', async () => {
|
||||
await supertest.get('/audit_log?query=param').set('kbn-xsrf', 'foo').expect(204);
|
||||
await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty());
|
||||
|
||||
const content = await logFile.readJSON();
|
||||
|
||||
const httpEvent = content.find((c) => c.event.action === 'http_request');
|
||||
expect(httpEvent).to.be.ok();
|
||||
expect(httpEvent.trace.id).to.be.ok();
|
||||
expect(httpEvent.user.name).to.be(username);
|
||||
expect(httpEvent.kibana.space_id).to.be('default');
|
||||
expect(httpEvent.http.request.method).to.be('get');
|
||||
expect(httpEvent.url.path).to.be('/audit_log');
|
||||
expect(httpEvent.url.query).to.be('query=param');
|
||||
|
||||
const createEvent = content.find((c) => c.event.action === 'saved_object_create');
|
||||
expect(createEvent).to.be.ok();
|
||||
expect(createEvent.trace.id).to.be.ok();
|
||||
expect(createEvent.user.name).to.be(username);
|
||||
expect(createEvent.kibana.space_id).to.be('default');
|
||||
|
||||
const findEvent = content.find((c) => c.event.action === 'saved_object_find');
|
||||
expect(findEvent).to.be.ok();
|
||||
expect(findEvent.trace.id).to.be.ok();
|
||||
expect(findEvent.user.name).to.be(username);
|
||||
expect(findEvent.kibana.space_id).to.be('default');
|
||||
});
|
||||
|
||||
it('logs audit events when logging in successfully', async () => {
|
||||
await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: { username, password },
|
||||
})
|
||||
.expect(200);
|
||||
await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty());
|
||||
|
||||
const content = await logFile.readJSON();
|
||||
|
||||
const loginEvent = content.find((c) => c.event.action === 'user_login');
|
||||
expect(loginEvent).to.be.ok();
|
||||
expect(loginEvent.event.outcome).to.be('success');
|
||||
expect(loginEvent.trace.id).to.be.ok();
|
||||
expect(loginEvent.user.name).to.be(username);
|
||||
});
|
||||
|
||||
it('logs audit events when failing to log in', async () => {
|
||||
await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: { username, password: 'invalid_password' },
|
||||
})
|
||||
.expect(401);
|
||||
await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty());
|
||||
|
||||
const content = await logFile.readJSON();
|
||||
|
||||
const loginEvent = content.find((c) => c.event.action === 'user_login');
|
||||
expect(loginEvent).to.be.ok();
|
||||
expect(loginEvent.event.outcome).to.be('failure');
|
||||
expect(loginEvent.trace.id).to.be.ok();
|
||||
expect(loginEvent.user).not.to.be.ok();
|
||||
});
|
||||
});
|
||||
}
|
14
x-pack/test/security_api_integration/tests/audit/index.ts
Normal file
14
x-pack/test/security_api_integration/tests/audit/index.ts
Normal file
|
@ -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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('security APIs - Audit Log', function () {
|
||||
this.tags('ciGroup6');
|
||||
loadTestFile(require.resolve('./audit_log'));
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue