diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 0744112650c23..1d5dfa8dd57c7 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -261,6 +261,7 @@
/x-pack/test/ui_capabilities/ @elastic/kibana-security
/x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security
/x-pack/test/functional/apps/security/ @elastic/kibana-security
+/x-pack/test/saved_object_access_control/ @elastic/kibana-security
/x-pack/test/security_api_integration/ @elastic/kibana-security
/x-pack/test/security_functional/ @elastic/kibana-security
/x-pack/test/spaces_api_integration/ @elastic/kibana-security
diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md
index d743508e046ea..f1024f3f2359e 100644
--- a/docs/development/core/public/kibana-plugin-core-public.md
+++ b/docs/development/core/public/kibana-plugin-core-public.md
@@ -103,6 +103,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [PluginInitializerContext](./kibana-plugin-core-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer
|
| [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) | This interface is a very simple wrapper for SavedObjects resolved from the server with the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md). |
| [SavedObject](./kibana-plugin-core-public.savedobject.md) | |
+| [SavedObjectAccessControl](./kibana-plugin-core-public.savedobjectaccesscontrol.md) | The "Access Control" describing which users should be authorized to access this SavedObject. |
| [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes
property. |
| [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | |
| [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. |
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.accesscontrol.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.accesscontrol.md
new file mode 100644
index 0000000000000..c33a64486020f
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.accesscontrol.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [accessControl](./kibana-plugin-core-public.savedobject.accesscontrol.md)
+
+## SavedObject.accessControl property
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md
index 26f472b741268..ef043b9cb18f9 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md
@@ -14,6 +14,7 @@ export interface SavedObject
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-public.savedobject.accesscontrol.md) | SavedObjectAccessControl
| |
| [attributes](./kibana-plugin-core-public.savedobject.attributes.md) | T
| The data for a Saved Object is stored as an object in the attributes
property. |
| [coreMigrationVersion](./kibana-plugin-core-public.savedobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. |
| [error](./kibana-plugin-core-public.savedobject.error.md) | SavedObjectError
| |
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.md
new file mode 100644
index 0000000000000..ad1b391f54389
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectAccessControl](./kibana-plugin-core-public.savedobjectaccesscontrol.md)
+
+## SavedObjectAccessControl interface
+
+The "Access Control" describing which users should be authorized to access this SavedObject.
+
+Signature:
+
+```typescript
+export interface SavedObjectAccessControl
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [owner](./kibana-plugin-core-public.savedobjectaccesscontrol.owner.md) | string
| The owner of this SavedObject. |
+
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.owner.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.owner.md
new file mode 100644
index 0000000000000..6781589367d87
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectaccesscontrol.owner.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectAccessControl](./kibana-plugin-core-public.savedobjectaccesscontrol.md) > [owner](./kibana-plugin-core-public.savedobjectaccesscontrol.owner.md)
+
+## SavedObjectAccessControl.owner property
+
+The owner of this SavedObject.
+
+Signature:
+
+```typescript
+owner: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.accesscontrol.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.accesscontrol.md
new file mode 100644
index 0000000000000..5abba34423fc8
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.accesscontrol.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [accessControl](./kibana-plugin-core-public.savedobjectreferencewithcontext.accesscontrol.md)
+
+## SavedObjectReferenceWithContext.accessControl property
+
+The access control of the referenced object
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md
index 722b11f0c7ba9..c75fbe49d6f86 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md
@@ -12,6 +12,7 @@ References to this object; note that this does not contain \_all inbound referen
inboundReferences: Array<{
type: string;
id: string;
+ accessControl?: SavedObjectAccessControl;
name: string;
}>;
```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md
index a79fa96695e36..7d4b9bb31d31c 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md
@@ -16,8 +16,9 @@ export interface SavedObjectReferenceWithContext
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-public.savedobjectreferencewithcontext.accesscontrol.md) | SavedObjectAccessControl
| The access control of the referenced object |
| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string
| The ID of the referenced object |
-| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}>
| References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
+| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
accessControl?: SavedObjectAccessControl;
name: string;
}>
| References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean
| Whether or not this object or reference is missing |
| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string[]
| The space(s) that the referenced object exists in |
| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[]
| The space(s) that legacy URL aliases matching this type/id exist in |
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index a26f8bd7b1594..46e38e8ce622b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -149,6 +149,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [RouteValidatorConfig](./kibana-plugin-core-server.routevalidatorconfig.md) | The configuration object to the RouteValidator class. Set params
, query
and/or body
to specify the validation logic to follow for that property. |
| [RouteValidatorOptions](./kibana-plugin-core-server.routevalidatoroptions.md) | Additional options for the RouteValidator class to modify its default behaviour. |
| [SavedObject](./kibana-plugin-core-server.savedobject.md) | |
+| [SavedObjectAccessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) | The "Access Control" describing which users should be authorized to access this SavedObject. |
| [SavedObjectAttributes](./kibana-plugin-core-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes
property. |
| [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) | |
| [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) |
@@ -163,6 +164,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md) | |
| [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | |
| [SavedObjectsCheckConflictsObject](./kibana-plugin-core-server.savedobjectscheckconflictsobject.md) | |
+| [SavedObjectsCheckConflictsOptions](./kibana-plugin-core-server.savedobjectscheckconflictsoptions.md) | |
| [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) | |
| [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. |
| [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
@@ -318,6 +320,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) |
| [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version |
| [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references
root property. |
+| [SavedObjectsBulkCreateOptions](./kibana-plugin-core-server.savedobjectsbulkcreateoptions.md) | |
| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError()
helpers exposed at SavedObjectsErrorHelpers
should be used to understand and manage error responses from the SavedObjectsClient
.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type
or doing substring checks on error.body.error.reason
, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) |
| [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. |
| [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.accesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.accesscontrol.md
new file mode 100644
index 0000000000000..cd55e571cb3eb
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.accesscontrol.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [accessControl](./kibana-plugin-core-server.savedobject.accesscontrol.md)
+
+## SavedObject.accessControl property
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md
index 4c62b359b284d..93ca1ee078b5c 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md
@@ -14,6 +14,7 @@ export interface SavedObject
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-server.savedobject.accesscontrol.md) | SavedObjectAccessControl
| |
| [attributes](./kibana-plugin-core-server.savedobject.attributes.md) | T
| The data for a Saved Object is stored as an object in the attributes
property. |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. |
| [error](./kibana-plugin-core-server.savedobject.error.md) | SavedObjectError
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.md
new file mode 100644
index 0000000000000..01d19205f520c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectAccessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md)
+
+## SavedObjectAccessControl interface
+
+The "Access Control" describing which users should be authorized to access this SavedObject.
+
+Signature:
+
+```typescript
+export interface SavedObjectAccessControl
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [owner](./kibana-plugin-core-server.savedobjectaccesscontrol.owner.md) | string
| The owner of this SavedObject. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.owner.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.owner.md
new file mode 100644
index 0000000000000..201c74fca5a26
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectaccesscontrol.owner.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectAccessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) > [owner](./kibana-plugin-core-server.savedobjectaccesscontrol.owner.md)
+
+## SavedObjectAccessControl.owner property
+
+The owner of this SavedObject.
+
+Signature:
+
+```typescript
+owner: string;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.accesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.accesscontrol.md
new file mode 100644
index 0000000000000..891744d945f8a
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.accesscontrol.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [accessControl](./kibana-plugin-core-server.savedobjectreferencewithcontext.accesscontrol.md)
+
+## SavedObjectReferenceWithContext.accessControl property
+
+The access control of the referenced object
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md
index 058c27032d065..598b10869f4d3 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md
@@ -12,6 +12,7 @@ References to this object; note that this does not contain \_all inbound referen
inboundReferences: Array<{
type: string;
id: string;
+ accessControl?: SavedObjectAccessControl;
name: string;
}>;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md
index 1f8b33c6e94e8..67f70c2c3176c 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md
@@ -16,8 +16,9 @@ export interface SavedObjectReferenceWithContext
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-server.savedobjectreferencewithcontext.accesscontrol.md) | SavedObjectAccessControl
| The access control of the referenced object |
| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string
| The ID of the referenced object |
-| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}>
| References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
+| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
accessControl?: SavedObjectAccessControl;
name: string;
}>
| References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation |
| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean
| Whether or not this object or reference is missing |
| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string[]
| The space(s) that the referenced object exists in |
| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[]
| The space(s) that legacy URL aliases matching this type/id exist in |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.accesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.accesscontrol.md
new file mode 100644
index 0000000000000..45df2a0c31a64
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.accesscontrol.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [accessControl](./kibana-plugin-core-server.savedobjectsbulkcreateobject.accesscontrol.md)
+
+## SavedObjectsBulkCreateObject.accessControl property
+
+The [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) to associate with this saved object.
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
index 463c3fe81b702..61311af30d728 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
@@ -15,6 +15,7 @@ export interface SavedObjectsBulkCreateObject
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-server.savedobjectsbulkcreateobject.accesscontrol.md) | SavedObjectAccessControl
| The [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) to associate with this saved object. |
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T
| |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateoptions.md
new file mode 100644
index 0000000000000..5a63f7351742c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateoptions.md
@@ -0,0 +1,12 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateOptions](./kibana-plugin-core-server.savedobjectsbulkcreateoptions.md)
+
+## SavedObjectsBulkCreateOptions type
+
+
+Signature:
+
+```typescript
+export declare type SavedObjectsBulkCreateOptions = Omit;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.accesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.accesscontrol.md
new file mode 100644
index 0000000000000..02af4f19a74af
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.accesscontrol.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsOptions](./kibana-plugin-core-server.savedobjectscheckconflictsoptions.md) > [accessControl](./kibana-plugin-core-server.savedobjectscheckconflictsoptions.accesscontrol.md)
+
+## SavedObjectsCheckConflictsOptions.accessControl property
+
+An [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) which should be compatible with conflicting objects.
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.md
new file mode 100644
index 0000000000000..74601b23b5e0e
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscheckconflictsoptions.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCheckConflictsOptions](./kibana-plugin-core-server.savedobjectscheckconflictsoptions.md)
+
+## SavedObjectsCheckConflictsOptions interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsCheckConflictsOptions extends SavedObjectsBaseOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-server.savedobjectscheckconflictsoptions.accesscontrol.md) | SavedObjectAccessControl
| An [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) which should be compatible with conflicting objects. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md
index 5cffb0c498b0b..dba5404171d18 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.checkconflicts.md
@@ -9,7 +9,7 @@ Check what conflicts will result when creating a given array of saved objects. T
Signature:
```typescript
-checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsCheckConflictsOptions): Promise;
```
## Parameters
@@ -17,7 +17,7 @@ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObje
| Parameter | Type | Description |
| --- | --- | --- |
| objects | SavedObjectsCheckConflictsObject[]
| |
-| options | SavedObjectsBaseOptions
| |
+| options | SavedObjectsCheckConflictsOptions
| |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.accesscontrol.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.accesscontrol.md
new file mode 100644
index 0000000000000..33130326c797d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.accesscontrol.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [accessControl](./kibana-plugin-core-server.savedobjectscreateoptions.accesscontrol.md)
+
+## SavedObjectsCreateOptions.accessControl property
+
+The [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) to associate with this saved object.
+
+Signature:
+
+```typescript
+accessControl?: SavedObjectAccessControl;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
index 7eaa9c51f5c82..209b976015807 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
@@ -15,6 +15,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
| Property | Type | Description |
| --- | --- | --- |
+| [accessControl](./kibana-plugin-core-server.savedobjectscreateoptions.accesscontrol.md) | SavedObjectAccessControl
| The [accessControl](./kibana-plugin-core-server.savedobjectaccesscontrol.md) to associate with this saved object. |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string
| (not recommended) Specify an id for the document |
| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createincompatibleaccesscontrolerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createincompatibleaccesscontrolerror.md
new file mode 100644
index 0000000000000..740021e4fbd79
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createincompatibleaccesscontrolerror.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createIncompatibleAccessControlError](./kibana-plugin-core-server.savedobjectserrorhelpers.createincompatibleaccesscontrolerror.md)
+
+## SavedObjectsErrorHelpers.createIncompatibleAccessControlError() method
+
+Signature:
+
+```typescript
+static createIncompatibleAccessControlError(type: string, id: string): DecoratedError;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| type | string
| |
+| id | string
| |
+
+Returns:
+
+`DecoratedError`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md
index 2dc78f2df3a83..e70a8fdbeb1b1 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md
@@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers
| [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static
| |
| [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static
| |
| [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static
| |
+| [createIncompatibleAccessControlError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createincompatibleaccesscontrolerror.md) | static
| |
| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static
| |
| [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static
| |
| [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md
index 17daf3ab1f042..cda7685a62abb 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md
@@ -9,7 +9,7 @@ Creates multiple documents at once
Signature:
```typescript
-bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
+bulkCreate(objects: Array>, options?: SavedObjectsBulkCreateOptions): Promise>;
```
## Parameters
@@ -17,7 +17,7 @@ bulkCreate(objects: Array>, options
| Parameter | Type | Description |
| --- | --- | --- |
| objects | Array<SavedObjectsBulkCreateObject<T>>
| |
-| options | SavedObjectsCreateOptions
| |
+| options | SavedObjectsBulkCreateOptions
| |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md
index 6e44bd704d6a7..aee3e4b408167 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md
@@ -9,7 +9,7 @@ Check what conflicts will result when creating a given array of saved objects. T
Signature:
```typescript
-checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsCheckConflictsOptions): Promise;
```
## Parameters
@@ -17,7 +17,7 @@ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObje
| Parameter | Type | Description |
| --- | --- | --- |
| objects | SavedObjectsCheckConflictsObject[]
| |
-| options | SavedObjectsBaseOptions
| |
+| options | SavedObjectsCheckConflictsOptions
| |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.accessclassification.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.accessclassification.md
new file mode 100644
index 0000000000000..afa38594095f5
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.accessclassification.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [accessClassification](./kibana-plugin-core-server.savedobjectstype.accessclassification.md)
+
+## SavedObjectsType.accessClassification property
+
+The for the type.
+
+Signature:
+
+```typescript
+accessClassification?: SavedObjectsAccessClassification;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md
index c3aba5261561f..729e1e9e23e1a 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md
@@ -18,6 +18,7 @@ This is only internal for now, and will only be public when we expose the regist
| Property | Type | Description |
| --- | --- | --- |
+| [accessClassification](./kibana-plugin-core-server.savedobjectstype.accessclassification.md) | SavedObjectsAccessClassification
| The for the type. |
| [convertToAliasScript](./kibana-plugin-core-server.savedobjectstype.converttoaliasscript.md) | string
| If defined, will be used to convert the type to an alias. |
| [convertToMultiNamespaceTypeVersion](./kibana-plugin-core-server.savedobjectstype.converttomultinamespacetypeversion.md) | string
| If defined, objects of this type will be converted to a 'multiple' or 'multiple-isolated' namespace type when migrating to this version.Requirements:1. This string value must be a valid semver version 2. This type must have previously specified [\`namespaceType: 'single'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) 3. This type must also specify [\`namespaceType: 'multiple'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md) \*or\* [\`namespaceType: 'multiple-isolated'\`](./kibana-plugin-core-server.savedobjectsnamespacetype.md)Example of a single-namespace type in 7.12:
```ts
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isprivate.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isprivate.md
new file mode 100644
index 0000000000000..322a3bdbedd1b
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.isprivate.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [isPrivate](./kibana-plugin-core-server.savedobjecttyperegistry.isprivate.md)
+
+## SavedObjectTypeRegistry.isPrivate() method
+
+Returns `true` if the given type is marked as `private`, and `false` otherwise.
+
+Signature:
+
+```typescript
+isPrivate(type: string): boolean;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| type | string
| |
+
+Returns:
+
+`boolean`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
index 0f2de8c8ef9b3..308cde47659db 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
@@ -25,6 +25,7 @@ export declare class SavedObjectTypeRegistry
| [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable
property for given type, or false
if the type is not registered or does not define a management section. |
| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable \*or\* isolated); resolves to false
if the type is not registered |
| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns whether the type is namespace-agnostic (global); resolves to false
if the type is not registered |
+| [isPrivate(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isprivate.md) | | Returns true
if the given type is marked as private
, and false
otherwise. |
| [isShareable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isshareable.md) | | Returns whether the type is multi-namespace (shareable); resolves to false
if the type is not registered |
| [isSingleNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) | | Returns whether the type is single-namespace (isolated); resolves to true
if the type is not registered |
| [registerType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.registertype.md) | | Register a [type](./kibana-plugin-core-server.savedobjectstype.md) inside the registry. A type can only be registered once. subsequent calls with the same type name will throw an error. |
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 40304d27580ca..c955fa7c15825 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -102,6 +102,7 @@ export type {
export { SimpleSavedObject } from './saved_objects';
export type { ResolvedSimpleSavedObject } from './saved_objects';
export type {
+ SavedObjectAccessControl,
SavedObjectsBatchResponse,
SavedObjectsBulkCreateObject,
SavedObjectsBulkCreateOptions,
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 673cb2e7ab557..13d93c39a3960 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -38,6 +38,7 @@ import React from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Request } from '@hapi/hapi';
import * as Rx from 'rxjs';
+import { SavedObjectAccessControl as SavedObjectAccessControl_2 } from 'src/core/types';
import { SchemaTypeError } from '@kbn/config-schema';
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
@@ -1169,6 +1170,8 @@ export interface ResolvedSimpleSavedObject {
//
// @public (undocumented)
export interface SavedObject {
+ // (undocumented)
+ accessControl?: SavedObjectAccessControl;
attributes: T;
coreMigrationVersion?: string;
// (undocumented)
@@ -1183,6 +1186,11 @@ export interface SavedObject {
version?: string;
}
+// @public
+export interface SavedObjectAccessControl {
+ owner: string;
+}
+
// @public
export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttributeSingle[];
@@ -1221,10 +1229,12 @@ export interface SavedObjectReference {
// @public
export interface SavedObjectReferenceWithContext {
+ accessControl?: SavedObjectAccessControl;
id: string;
inboundReferences: Array<{
type: string;
id: string;
+ accessControl?: SavedObjectAccessControl;
name: string;
}>;
isMissing?: boolean;
diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts
index bd22947b174b7..333a1b4a76a42 100644
--- a/src/core/public/saved_objects/index.ts
+++ b/src/core/public/saved_objects/index.ts
@@ -24,6 +24,7 @@ export { SimpleSavedObject } from './simple_saved_object';
export type { ResolvedSimpleSavedObject } from './types';
export type { SavedObjectsStart } from './saved_objects_service';
export type {
+ SavedObjectAccessControl,
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
SavedObjectsFindOptionsReference,
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index c77a3a967364c..b255f14404ca6 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -293,13 +293,16 @@ export {
} from './saved_objects';
export type {
+ SavedObjectAccessControl,
SavedObjectsBulkCreateObject,
+ SavedObjectsBulkCreateOptions,
SavedObjectsBulkGetObject,
SavedObjectsBulkUpdateObject,
SavedObjectsBulkUpdateOptions,
SavedObjectsBulkResponse,
SavedObjectsBulkUpdateResponse,
SavedObjectsCheckConflictsObject,
+ SavedObjectsCheckConflictsOptions,
SavedObjectsCheckConflictsResponse,
SavedObjectsClientProviderOptions,
SavedObjectsClientWrapperFactory,
diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts
index 211dcdc4ee62d..154d43913bf40 100644
--- a/src/core/server/saved_objects/export/saved_objects_exporter.ts
+++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts
@@ -129,7 +129,9 @@ export class SavedObjectsExporter {
// redact attributes that should not be exported
const redactedObjects = includeNamespaces
? exportedObjects
- : exportedObjects.map>(({ namespaces, ...object }) => object);
+ : exportedObjects.map>(
+ ({ namespaces, accessControl, ...object }) => object
+ );
const exportDetails: SavedObjectsExportResultDetails = {
exportedCount: exportedObjects.length,
diff --git a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
index 3370dda05f68b..65789a89da238 100644
--- a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
+++ b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
@@ -102,7 +102,9 @@ describe('#checkConflicts', () => {
it('returns expected result', async () => {
const namespace = 'foo-namespace';
const params = setupParams({ objects, namespace });
- socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] });
+ socCheckConflicts.mockResolvedValue({
+ errors: [obj2Error, obj3Error, obj4Error],
+ });
const checkConflictsResult = await checkConflicts(params);
expect(checkConflictsResult).toEqual({
@@ -129,7 +131,9 @@ describe('#checkConflicts', () => {
it('does not return errors for regular conflicts when ignoreRegularConflicts=true', async () => {
const namespace = 'foo-namespace';
const params = setupParams({ objects, namespace, ignoreRegularConflicts: true });
- socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] });
+ socCheckConflicts.mockResolvedValue({
+ errors: [obj2Error, obj3Error, obj4Error],
+ });
const checkConflictsResult = await checkConflicts(params);
expect(checkConflictsResult).toEqual(
@@ -197,7 +201,9 @@ describe('#checkConflicts', () => {
it('adds `omitOriginId` field to `importIdMap` entries when createNewCopies=true', async () => {
const namespace = 'foo-namespace';
const params = setupParams({ objects, namespace, createNewCopies: true });
- socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] });
+ socCheckConflicts.mockResolvedValue({
+ errors: [obj2Error, obj3Error, obj4Error],
+ });
const checkConflictsResult = await checkConflicts(params);
expect(checkConflictsResult).toEqual(
diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts
index 5f853d49219dc..8b0cbd93724a5 100644
--- a/src/core/server/saved_objects/index.ts
+++ b/src/core/server/saved_objects/index.ts
@@ -86,6 +86,7 @@ export type {
} from './migrations';
export type {
+ SavedObjectAccessControl,
SavedObjectsNamespaceType,
SavedObjectStatusMeta,
SavedObjectsType,
diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
index 9ee998118bde6..616cd7e1f7a26 100644
--- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
+++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
@@ -5,6 +5,7 @@ Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
"aaa": "625b32086eb1d1203564cf85062dd22e",
+ "accessControl": "f759893589b96eeddcb456de15abb5f4",
"bbb": "18c78c995965207ed3f6e7fc5c6e55fe",
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
@@ -21,6 +22,15 @@ Object {
"aaa": Object {
"type": "text",
},
+ "accessControl": Object {
+ "dynamic": "strict",
+ "properties": Object {
+ "owner": Object {
+ "type": "keyword",
+ },
+ },
+ "type": "object",
+ },
"bbb": Object {
"type": "long",
},
@@ -68,6 +78,7 @@ exports[`buildActiveMappings handles the \`dynamic\` property of types 1`] = `
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
+ "accessControl": "f759893589b96eeddcb456de15abb5f4",
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
"firstType": "635418ab953d81d93f1190b70a8d3f57",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
@@ -83,6 +94,15 @@ Object {
},
"dynamic": "strict",
"properties": Object {
+ "accessControl": Object {
+ "dynamic": "strict",
+ "properties": Object {
+ "owner": Object {
+ "type": "keyword",
+ },
+ },
+ "type": "object",
+ },
"coreMigrationVersion": Object {
"type": "keyword",
},
diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
index d3ac00fc0354a..fe3dff0c64c04 100644
--- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
+++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
@@ -127,6 +127,15 @@ function defaultMapping(): IndexMapping {
type: {
type: 'keyword',
},
+ accessControl: {
+ type: 'object',
+ dynamic: 'strict',
+ properties: {
+ owner: {
+ type: 'keyword',
+ },
+ },
+ },
namespace: {
type: 'keyword',
},
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index 87b8ee0809064..5cec306ad38f9 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -280,9 +280,9 @@ describe('DocumentMigrator', () => {
const migrator = new DocumentMigrator({
...testOpts(),
typeRegistry: createRegistry({
- name: 'acl',
+ name: 'accessControl',
migrations: {
- '2.3.5': setAttr('acl', 'admins-only, sucka!'),
+ '2.3.5': setAttr('accessControl', 'admins-only, sucka!'),
},
}),
});
@@ -291,15 +291,15 @@ describe('DocumentMigrator', () => {
id: 'me',
type: 'user',
attributes: { name: 'Tyler' },
- acl: 'anyone',
+ accessControl: { owner: 'anyone' },
migrationVersion: {},
} as SavedObjectUnsanitizedDoc);
expect(actual).toEqual({
id: 'me',
type: 'user',
attributes: { name: 'Tyler' },
- migrationVersion: { acl: '2.3.5' },
- acl: 'admins-only, sucka!',
+ migrationVersion: { accessControl: '2.3.5' },
+ accessControl: 'admins-only, sucka!',
coreMigrationVersion: kibanaVersion,
});
});
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index 64d4fa3609e90..4b1e47fb0b3f5 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -61,6 +61,7 @@ describe('IndexMigrator', () => {
namespace: '2f4316de49999235636386fe51dc06c1',
namespaces: '2f4316de49999235636386fe51dc06c1',
originId: '2f4316de49999235636386fe51dc06c1',
+ accessControl: 'f759893589b96eeddcb456de15abb5f4',
references: '7997cf5a56cc02bdc9c93361bde732b0',
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
type: '2f4316de49999235636386fe51dc06c1',
@@ -75,6 +76,15 @@ describe('IndexMigrator', () => {
originId: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ accessControl: {
+ dynamic: 'strict',
+ properties: {
+ owner: {
+ type: 'keyword',
+ },
+ },
+ type: 'object',
+ },
references: {
type: 'nested',
properties: {
@@ -186,6 +196,7 @@ describe('IndexMigrator', () => {
originId: '2f4316de49999235636386fe51dc06c1',
references: '7997cf5a56cc02bdc9c93361bde732b0',
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
+ accessControl: 'f759893589b96eeddcb456de15abb5f4',
type: '2f4316de49999235636386fe51dc06c1',
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
},
@@ -199,6 +210,15 @@ describe('IndexMigrator', () => {
originId: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ accessControl: {
+ dynamic: 'strict',
+ properties: {
+ owner: {
+ type: 'keyword',
+ },
+ },
+ type: 'object',
+ },
references: {
type: 'nested',
properties: {
@@ -247,6 +267,7 @@ describe('IndexMigrator', () => {
namespace: '2f4316de49999235636386fe51dc06c1',
namespaces: '2f4316de49999235636386fe51dc06c1',
originId: '2f4316de49999235636386fe51dc06c1',
+ accessControl: 'f759893589b96eeddcb456de15abb5f4',
references: '7997cf5a56cc02bdc9c93361bde732b0',
coreMigrationVersion: '2f4316de49999235636386fe51dc06c1',
type: '2f4316de49999235636386fe51dc06c1',
@@ -262,6 +283,15 @@ describe('IndexMigrator', () => {
originId: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ accessControl: {
+ dynamic: 'strict',
+ properties: {
+ owner: {
+ type: 'keyword',
+ },
+ },
+ type: 'object',
+ },
references: {
type: 'nested',
properties: {
diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
index 32c2536ab0296..ac2cb37be4cb3 100644
--- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
+++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
@@ -4,6 +4,7 @@ exports[`KibanaMigrator getActiveMappings returns full index mappings w/ core pr
Object {
"_meta": Object {
"migrationMappingPropertyHashes": Object {
+ "accessControl": "f759893589b96eeddcb456de15abb5f4",
"amap": "510f1f0adb69830cf8a1c5ce2923ed82",
"bmap": "510f1f0adb69830cf8a1c5ce2923ed82",
"coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
@@ -18,6 +19,15 @@ Object {
},
"dynamic": "strict",
"properties": Object {
+ "accessControl": Object {
+ "dynamic": "strict",
+ "properties": Object {
+ "owner": Object {
+ "type": "keyword",
+ },
+ },
+ "type": "object",
+ },
"amap": Object {
"properties": Object {
"field": Object {
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
index d53a53d745c0c..ea4e1f024339d 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
@@ -24,6 +24,7 @@ const createRegistryMock = (): jest.Mocked<
isHidden: jest.fn(),
getIndex: jest.fn(),
isImportableAndExportable: jest.fn(),
+ isPrivate: jest.fn(),
};
mock.getVisibleTypes.mockReturnValue([]);
@@ -39,6 +40,7 @@ const createRegistryMock = (): jest.Mocked<
mock.isMultiNamespace.mockImplementation((type: string) => type === 'shared');
mock.isShareable.mockImplementation((type: string) => type === 'shared');
mock.isImportableAndExportable.mockReturnValue(true);
+ mock.isPrivate.mockReturnValue(false);
return mock;
};
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
index 872b61706c526..8c2d24051553c 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
@@ -330,6 +330,22 @@ describe('SavedObjectTypeRegistry', () => {
});
});
+ describe('#isPrivate', () => {
+ it('returns correct value for the type', () => {
+ registry.registerType(createType({ name: 'typeA', accessClassification: 'private' }));
+ registry.registerType(createType({ name: 'typeB', accessClassification: 'public' }));
+
+ expect(registry.isPrivate('typeA')).toEqual(true);
+ expect(registry.isPrivate('typeB')).toEqual(false);
+ });
+ it('returns false when the type is not registered', () => {
+ registry.registerType(createType({ name: 'typeA', accessClassification: 'private' }));
+ registry.registerType(createType({ name: 'typeB', accessClassification: 'public' }));
+
+ expect(registry.isPrivate('unknownType')).toEqual(false);
+ });
+ });
+
describe('#getIndex', () => {
it('returns correct value for the type', () => {
registry.registerType(createType({ name: 'typeA', indexPattern: '.custom-index' }));
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts
index ba5960c59239d..47073ed361657 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.ts
@@ -125,6 +125,13 @@ export class SavedObjectTypeRegistry {
public isImportableAndExportable(type: string) {
return this.types.get(type)?.management?.importableAndExportable ?? false;
}
+
+ /**
+ * Returns `true` if the given type is marked as `private`, and `false` otherwise.
+ */
+ public isPrivate(type: string) {
+ return this.types.get(type)?.accessClassification === 'private' ?? false;
+ }
}
const validateType = ({ name, management }: SavedObjectsType) => {
diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts
index 3fdeb4aa088e1..924f9512eaaa4 100644
--- a/src/core/server/saved_objects/serialization/serializer.test.ts
+++ b/src/core/server/saved_objects/serialization/serializer.test.ts
@@ -75,13 +75,13 @@ describe('#rawToSavedObject', () => {
type: 'foo',
migrationVersion: {
hello: '1.2.3',
- acl: '33.3.5',
+ accessControl: '33.3.5',
},
},
});
expect(actual).toHaveProperty('migrationVersion', {
hello: '1.2.3',
- acl: '33.3.5',
+ accessControl: '33.3.5',
});
});
@@ -109,7 +109,7 @@ describe('#rawToSavedObject', () => {
},
migrationVersion: {
hello: '1.2.3',
- acl: '33.3.5',
+ accessControl: '33.3.5',
},
updated_at: now,
},
@@ -124,7 +124,7 @@ describe('#rawToSavedObject', () => {
},
migrationVersion: {
hello: '1.2.3',
- acl: '33.3.5',
+ accessControl: '33.3.5',
},
updated_at: now,
references: [],
@@ -132,6 +132,27 @@ describe('#rawToSavedObject', () => {
expect(expected).toEqual(actual);
});
+ test('if specified it copies the _source.accessControl property to accessControl', () => {
+ const actual = singleNamespaceSerializer.rawToSavedObject({
+ _id: 'foo:bar',
+ _source: {
+ type: 'foo',
+ accessControl: { owner: 'alice' },
+ },
+ });
+ expect(actual).toHaveProperty('accessControl', { owner: 'alice' });
+ });
+
+ test(`if _source.accessControl is unspecified it doesn't set accessControl`, () => {
+ const actual = singleNamespaceSerializer.rawToSavedObject({
+ _id: 'foo:bar',
+ _source: {
+ type: 'foo',
+ },
+ });
+ expect(actual).not.toHaveProperty('accessControl');
+ });
+
test('if specified it copies the _source.coreMigrationVersion property to coreMigrationVersion', () => {
const actual = singleNamespaceSerializer.rawToSavedObject({
_id: 'foo:bar',
diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts
index 9c91abcfe79c5..8278e63a0f224 100644
--- a/src/core/server/saved_objects/serialization/serializer.ts
+++ b/src/core/server/saved_objects/serialization/serializer.ts
@@ -91,6 +91,7 @@ export class SavedObjectsSerializer {
migrationVersion,
references,
coreMigrationVersion,
+ accessControl,
} = _source;
const version =
@@ -108,6 +109,7 @@ export class SavedObjectsSerializer {
...(includeNamespace && { namespace }),
...(includeNamespaces && { namespaces }),
...(originId && { originId }),
+ ...(accessControl && { accessControl }),
attributes: _source[type],
references: references || [],
...(migrationVersion && { migrationVersion }),
@@ -136,6 +138,7 @@ export class SavedObjectsSerializer {
version,
references,
coreMigrationVersion,
+ accessControl,
} = savedObj;
const source = {
[type]: attributes,
@@ -144,6 +147,7 @@ export class SavedObjectsSerializer {
...(namespace && this.registry.isSingleNamespace(type) && { namespace }),
...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }),
...(originId && { originId }),
+ ...(accessControl && { accessControl }),
...(migrationVersion && { migrationVersion }),
...(coreMigrationVersion && { coreMigrationVersion }),
...(updated_at && { updated_at }),
diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts
index 3956e2133e5a8..f2f788126a703 100644
--- a/src/core/server/saved_objects/serialization/types.ts
+++ b/src/core/server/saved_objects/serialization/types.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { SavedObjectAccessControl } from 'src/core/types';
import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types';
/**
@@ -29,6 +30,7 @@ export interface SavedObjectsRawDocSource {
updated_at?: string;
references?: SavedObjectReference[];
originId?: string;
+ accessControl?: SavedObjectAccessControl;
[typeMapping: string]: any;
}
@@ -47,6 +49,7 @@ interface SavedObjectDoc {
version?: string;
updated_at?: string;
originId?: string;
+ accessControl?: SavedObjectAccessControl;
}
interface Referencable {
diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts
index 43923695f6548..25d5065c4744c 100644
--- a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts
+++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts
@@ -12,7 +12,7 @@ import { esKuery } from '../../es_query';
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import type { SavedObjectsSerializer } from '../../serialization';
-import type { SavedObject, SavedObjectsBaseOptions } from '../../types';
+import type { SavedObject, SavedObjectsBaseOptions, SavedObjectAccessControl } from '../../types';
import { getRootFields } from './included_fields';
import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils';
import type {
@@ -68,6 +68,8 @@ export interface SavedObjectReferenceWithContext {
type: string;
/** The ID of the referenced object */
id: string;
+ /** The access control of the referenced object */
+ accessControl?: SavedObjectAccessControl;
/** The space(s) that the referenced object exists in */
spaces: string[];
/**
@@ -79,6 +81,8 @@ export interface SavedObjectReferenceWithContext {
type: string;
/** The ID of the object that has the inbound reference */
id: string;
+ /** The access control specification of the object that has the inbound reference */
+ accessControl?: SavedObjectAccessControl;
/** The name of the inbound reference */
name: string;
}>;
@@ -138,7 +142,14 @@ export async function collectMultiNamespaceReferences(
const { type, id } = parseKey(referenceKey);
const object = objectMap.get(referenceKey);
const spaces = object?.namespaces ?? [];
- return { type, id, spaces, inboundReferences, ...(object === null && { isMissing: true }) };
+ return {
+ type,
+ id,
+ accessControl: object?.accessControl,
+ spaces,
+ inboundReferences,
+ ...(object === null && { isMissing: true }),
+ };
});
const aliasesMap = await checkLegacyUrlAliases(createPointInTimeFinder, objectsWithContext);
diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts
index a366dce626ec2..a783be6cd7e6f 100644
--- a/src/core/server/saved_objects/service/lib/errors.test.ts
+++ b/src/core/server/saved_objects/service/lib/errors.test.ts
@@ -263,6 +263,38 @@ describe('savedObjectsClient/errorTypes', () => {
});
});
+ describe('Incompatible accessControl error', () => {
+ describe('createIncompatibleAccessControlError', () => {
+ it('makes the error identifiable as a Conflict error', () => {
+ const error = SavedObjectsErrorHelpers.createIncompatibleAccessControlError('type', 'id');
+ expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(true);
+ });
+
+ it('adds boom properties', () => {
+ const error = SavedObjectsErrorHelpers.createIncompatibleAccessControlError('type', 'id');
+ expect(error).toHaveProperty('isBoom', true);
+ });
+
+ describe('error.output', () => {
+ it('prefixes message with reason', () => {
+ const error = SavedObjectsErrorHelpers.createIncompatibleAccessControlError('type', 'id');
+ expect(error.output.payload).toMatchInlineSnapshot(`
+ Object {
+ "error": "Conflict",
+ "message": "Saved object [type/id] conflict: incompatible accessControl",
+ "statusCode": 409,
+ }
+ `);
+ });
+
+ it('sets statusCode to 409', () => {
+ const error = SavedObjectsErrorHelpers.createIncompatibleAccessControlError('type', 'id');
+ expect(error.output).toHaveProperty('statusCode', 409);
+ });
+ });
+ });
+ });
+
describe('TooManyRequests error', () => {
describe('decorateTooManyRequestsError', () => {
it('returns original object', () => {
diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts
index 581145c7c09d1..b90a78351d1e4 100644
--- a/src/core/server/saved_objects/service/lib/errors.ts
+++ b/src/core/server/saved_objects/service/lib/errors.ts
@@ -163,6 +163,12 @@ export class SavedObjectsErrorHelpers {
);
}
+ public static createIncompatibleAccessControlError(type: string, id: string) {
+ return SavedObjectsErrorHelpers.decorateConflictError(
+ Boom.conflict(`Saved object [${type}/${id}] conflict: incompatible accessControl`)
+ );
+ }
+
public static isConflictError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT;
}
diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts
index a41a25a27b70d..c0dd1fb19deda 100644
--- a/src/core/server/saved_objects/service/lib/filter_utils.ts
+++ b/src/core/server/saved_objects/service/lib/filter_utils.ts
@@ -55,7 +55,8 @@ export const validateConvertFilterToKueryNode = (
const existingKueryNode: KueryNode =
path.length === 0 ? filterKueryNode : get(filterKueryNode, path);
if (item.isSavedObjectAttr) {
- existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1];
+ const [, ...kueryNodeParts] = existingKueryNode.arguments[0].value.split('.');
+ existingKueryNode.arguments[0].value = kueryNodeParts.join('.');
const itemType = allowedTypes.filter((t) => t === item.type);
if (itemType.length === 1) {
set(
@@ -147,7 +148,8 @@ export const validateFilterKueryNode = ({
),
isSavedObjectAttr: isSavedObjectAttr(
nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value,
- indexMapping
+ indexMapping,
+ types
),
key: nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value,
type: getType(nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value),
@@ -168,15 +170,21 @@ const getType = (key: string | undefined | null) =>
* @param key
* @param indexMapping
*/
-export const isSavedObjectAttr = (key: string | null | undefined, indexMapping: IndexMapping) => {
+export const isSavedObjectAttr = (
+ key: string | null | undefined,
+ indexMapping: IndexMapping,
+ types: string[]
+) => {
const keySplit = key != null ? key.split('.') : [];
if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) {
return true;
- } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) {
- return true;
- } else {
- return false;
+ } else if (keySplit.length >= 2) {
+ const attributeKey = `${keySplit.slice(1, keySplit.length).join('.')}`;
+ if (!types.includes(keySplit[1]) && fieldDefined(indexMapping, attributeKey)) {
+ return true;
+ }
}
+ return false;
};
export const hasFilterKeyError = (
@@ -194,6 +202,9 @@ export const hasFilterKeyError = (
if (keySplit.length <= 1 || !types.includes(keySplit[0])) {
return `This type ${keySplit[0]} is not allowed`;
}
+ if (isSavedObjectAttr(key, indexMapping, types)) {
+ return null;
+ }
if (
(keySplit.length === 2 && fieldDefined(indexMapping, key)) ||
(keySplit.length > 2 && keySplit[1] !== 'attributes')
diff --git a/src/core/server/saved_objects/service/lib/internal_utils.test.ts b/src/core/server/saved_objects/service/lib/internal_utils.test.ts
index d1fd067990f07..da1cea68101c3 100644
--- a/src/core/server/saved_objects/service/lib/internal_utils.test.ts
+++ b/src/core/server/saved_objects/service/lib/internal_utils.test.ts
@@ -91,6 +91,7 @@ describe('#getSavedObjectFromSource', () => {
const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }];
const migrationVersion = { foo: 'migrationVersion' };
const coreMigrationVersion = 'coreMigrationVersion';
+ const accessControl = { owner: 'alice' };
const originId = 'originId';
// eslint-disable-next-line @typescript-eslint/naming-convention
const updated_at = 'updatedAt';
@@ -110,6 +111,7 @@ describe('#getSavedObjectFromSource', () => {
migrationVersion,
coreMigrationVersion,
originId,
+ accessControl,
updated_at,
...namespaceAttrs,
},
@@ -127,6 +129,7 @@ describe('#getSavedObjectFromSource', () => {
id,
migrationVersion,
namespaces: expect.anything(), // see specific test cases below
+ accessControl,
originId,
references,
type,
diff --git a/src/core/server/saved_objects/service/lib/internal_utils.ts b/src/core/server/saved_objects/service/lib/internal_utils.ts
index feaaea15649c7..426433c530a07 100644
--- a/src/core/server/saved_objects/service/lib/internal_utils.ts
+++ b/src/core/server/saved_objects/service/lib/internal_utils.ts
@@ -87,7 +87,7 @@ export function getSavedObjectFromSource(
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
): SavedObject {
- const { originId, updated_at: updatedAt } = doc._source;
+ const { originId, updated_at: updatedAt, accessControl } = doc._source;
let namespaces: string[] = [];
if (!registry.isNamespaceAgnostic(type)) {
@@ -100,6 +100,7 @@ export function getSavedObjectFromSource(
id,
type,
namespaces,
+ ...(accessControl && { accessControl }),
...(originId && { originId }),
...(updatedAt && { updated_at: updatedAt }),
version: encodeHitVersion(doc),
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 474721ff3610a..31b7a17695520 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -105,6 +105,13 @@ describe('SavedObjectsRepository', () => {
},
},
},
+ 'confidential-type': {
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ },
+ },
[CUSTOM_INDEX_TYPE]: {
properties: {
type: 'keyword',
@@ -148,16 +155,18 @@ describe('SavedObjectsRepository', () => {
},
};
- const createType = (type) => ({
+ const createType = (type, options = {}) => ({
name: type,
mappings: { properties: mappings.properties[type].properties },
migrations: { '1.1.1': (doc) => doc },
+ ...options,
});
const registry = new SavedObjectTypeRegistry();
registry.registerType(createType('config'));
registry.registerType(createType('index-pattern'));
registry.registerType(createType('dashboard'));
+ registry.registerType(createType('confidential-type', { accessClassification: 'confidential' }));
registry.registerType({
...createType(CUSTOM_INDEX_TYPE),
indexPattern: 'custom',
@@ -192,7 +201,7 @@ describe('SavedObjectsRepository', () => {
});
const getMockGetResponse = (
- { type, id, references, namespace: objectNamespace, originId },
+ { type, id, references, namespace: objectNamespace, originId, accessControl },
namespace
) => {
const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace;
@@ -207,6 +216,7 @@ describe('SavedObjectsRepository', () => {
...(registry.isSingleNamespace(type) && { namespace: namespaceId }),
...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }),
...(originId && { originId }),
+ ...(accessControl && { accessControl }),
type,
[type]: { title: 'Testing' },
references,
@@ -314,34 +324,48 @@ describe('SavedObjectsRepository', () => {
attributes: { title: 'Test Two' },
references: [{ name: 'ref_0', type: 'test', id: '2' }],
};
+ const obj3 = {
+ type: 'confidential-type',
+ id: 'my-secret',
+ accessControl: {
+ owner: 'alice',
+ },
+ attributes: { title: 'Test Three' },
+ references: [{ name: 'ref_0', type: 'test', id: '3' }],
+ };
const namespace = 'foo-namespace';
const getMockBulkCreateResponse = (objects, namespace) => {
return {
- items: objects.map(({ type, id, originId, attributes, references, migrationVersion }) => ({
- create: {
- _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`,
- _source: {
- [type]: attributes,
- type,
- namespace,
- ...(originId && { originId }),
- references,
- ...mockTimestampFields,
- migrationVersion: migrationVersion || { [type]: '1.1.1' },
+ items: objects.map(
+ ({ type, id, originId, attributes, references, migrationVersion, accessControl }) => ({
+ create: {
+ _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`,
+ _source: {
+ [type]: attributes,
+ type,
+ namespace,
+ ...(accessControl && { accessControl }),
+ ...(originId && { originId }),
+ references,
+ ...mockTimestampFields,
+ migrationVersion: migrationVersion || { [type]: '1.1.1' },
+ },
+ ...mockVersionProps,
},
- ...mockVersionProps,
- },
- })),
+ })
+ ),
};
};
const bulkCreateSuccess = async (objects, options) => {
- const multiNamespaceObjects = objects.filter(
- ({ type, id }) => registry.isMultiNamespace(type) && id
+ const preflightObjects = objects.filter(
+ ({ type, id }) =>
+ id &&
+ (registry.isMultiNamespace(type) || (options?.overwrite && registry.isPrivate(type)))
);
- if (multiNamespaceObjects?.length) {
- const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace);
+ if (preflightObjects.length) {
+ const response = getMockMgetResponse(preflightObjects, options?.namespace);
client.mget.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(response)
);
@@ -351,7 +375,7 @@ describe('SavedObjectsRepository', () => {
elasticsearchClientMock.createSuccessTransportRequestPromise(response)
);
const result = await savedObjectsRepository.bulkCreate(objects, options);
- expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0);
+ expect(client.mget).toHaveBeenCalledTimes(preflightObjects.length ? 1 : 0);
return result;
};
@@ -379,12 +403,13 @@ describe('SavedObjectsRepository', () => {
);
};
- const expectObjArgs = ({ type, attributes, references }, overrides) => [
+ const expectObjArgs = ({ type, attributes, references, accessControl }, overrides) => [
expect.any(Object),
expect.objectContaining({
[type]: attributes,
references,
type,
+ ...(accessControl ? { accessControl } : undefined),
...overrides,
...mockTimestampFields,
}),
@@ -401,12 +426,12 @@ describe('SavedObjectsRepository', () => {
describe('client calls', () => {
it(`should use the ES bulk action by default`, async () => {
- await bulkCreateSuccess([obj1, obj2]);
+ await bulkCreateSuccess([obj1, obj2, obj3]);
expect(client.bulk).toHaveBeenCalledTimes(1);
});
it(`should use the ES mget action before bulk action for any types that are multi-namespace, when id is defined`, async () => {
- const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }];
+ const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, obj3];
await bulkCreateSuccess(objects);
expect(client.bulk).toHaveBeenCalledTimes(1);
expect(client.mget).toHaveBeenCalledTimes(1);
@@ -417,13 +442,13 @@ describe('SavedObjectsRepository', () => {
});
it(`should use the ES create method if ID is undefined and overwrite=true`, async () => {
- const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
+ const objects = [obj1, obj2, obj3].map((obj) => ({ ...obj, id: undefined }));
await bulkCreateSuccess(objects, { overwrite: true });
expectClientCallArgsAction(objects, { method: 'create' });
});
it(`should use the ES create method if ID is undefined and overwrite=false`, async () => {
- const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
+ const objects = [obj1, obj2, obj3].map((obj) => ({ ...obj, id: undefined }));
await bulkCreateSuccess(objects);
expectClientCallArgsAction(objects, { method: 'create' });
});
@@ -441,6 +466,10 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
},
obj2,
+ {
+ ...obj3,
+ version: mockVersion,
+ },
],
{ overwrite: true }
);
@@ -451,7 +480,13 @@ describe('SavedObjectsRepository', () => {
if_primary_term: mockVersionProps._primary_term,
};
- expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' });
+ const obj3WithSeq = {
+ ...obj3,
+ if_seq_no: mockVersionProps._seq_no,
+ if_primary_term: mockVersionProps._primary_term,
+ };
+
+ expectClientCallArgsAction([obj1WithSeq, obj2, obj3WithSeq], { method: 'index' });
});
it(`should use the ES create method if ID is defined and overwrite=false`, async () => {
@@ -460,18 +495,39 @@ describe('SavedObjectsRepository', () => {
});
it(`formats the ES request`, async () => {
- await bulkCreateSuccess([obj1, obj2]);
- const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)];
+ await bulkCreateSuccess([obj1, obj2, obj3]);
+ const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2), ...expectObjArgs(obj3)];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`allows an access control specification to be specified`, async () => {
+ await bulkCreateSuccess([obj3]);
+ const body = [...expectObjArgs(obj3)];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
);
+ const [call] = client.bulk.mock.calls;
+ const [payload] = call;
+ const [, object] = payload.body;
+ expect(object).toHaveProperty('type', 'confidential-type');
+ expect(object).toHaveProperty('accessControl', { owner: 'alice' });
});
it(`adds namespace to request body for any types that are single-namespace`, async () => {
- await bulkCreateSuccess([obj1, obj2], { namespace });
+ await bulkCreateSuccess([obj1, obj2, obj3], { namespace });
const expected = expect.objectContaining({ namespace });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
+ const body = [
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ ];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
@@ -479,9 +535,16 @@ describe('SavedObjectsRepository', () => {
});
it(`normalizes options.namespace from 'default' to undefined`, async () => {
- await bulkCreateSuccess([obj1, obj2], { namespace: 'default' });
+ await bulkCreateSuccess([obj1, obj2, obj3], { namespace: 'default' });
const expected = expect.not.objectContaining({ namespace: 'default' });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
+ const body = [
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ ];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
@@ -492,10 +555,18 @@ describe('SavedObjectsRepository', () => {
const objects = [
{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
{ ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ { ...obj3, type: MULTI_NAMESPACE_TYPE },
];
await bulkCreateSuccess(objects, { namespace });
const expected = expect.not.objectContaining({ namespace: expect.anything() });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
+ const body = [
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ ];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
@@ -504,11 +575,21 @@ describe('SavedObjectsRepository', () => {
it(`adds namespaces to request body for any types that are multi-namespace`, async () => {
const test = async (namespace) => {
- const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE }));
+ const objects = [obj1, obj2, obj3].map((x) => ({
+ ...x,
+ type: MULTI_NAMESPACE_ISOLATED_TYPE,
+ }));
const namespaces = [namespace ?? 'default'];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const expected = expect.objectContaining({ namespaces });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
+ const body = [
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ expect.any(Object),
+ expected,
+ ];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
@@ -570,7 +651,7 @@ describe('SavedObjectsRepository', () => {
});
it(`defaults to a refresh setting of wait_for`, async () => {
- await bulkCreateSuccess([obj1, obj2]);
+ await bulkCreateSuccess([obj1, obj2, obj3]);
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ refresh: 'wait_for' }),
expect.anything()
@@ -578,25 +659,28 @@ describe('SavedObjectsRepository', () => {
});
it(`should use default index`, async () => {
- await bulkCreateSuccess([obj1, obj2]);
- expectClientCallArgsAction([obj1, obj2], { method: 'create', _index: '.kibana-test' });
+ await bulkCreateSuccess([obj1, obj2, obj3]);
+ expectClientCallArgsAction([obj1, obj2, obj3], {
+ method: 'create',
+ _index: '.kibana-test',
+ });
});
it(`should use custom index`, async () => {
- await bulkCreateSuccess([obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE })));
- expectClientCallArgsAction([obj1, obj2], { method: 'create', _index: 'custom' });
+ await bulkCreateSuccess([obj1, obj2, obj3].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE })));
+ expectClientCallArgsAction([obj1, obj2, obj3], { method: 'create', _index: 'custom' });
});
it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => {
const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix)
- await bulkCreateSuccess([obj1, obj2], { namespace });
- expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
+ await bulkCreateSuccess([obj1, obj2, obj3], { namespace });
+ expectClientCallArgsAction([obj1, obj2, obj3], { method: 'create', getId });
});
it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => {
const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix)
- await bulkCreateSuccess([obj1, obj2]);
- expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
+ await bulkCreateSuccess([obj1, obj2, obj3]);
+ expectClientCallArgsAction([obj1, obj2, obj3], { method: 'create', getId });
});
it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => {
@@ -604,6 +688,7 @@ describe('SavedObjectsRepository', () => {
const objects = [
{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
{ ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ { ...obj3, type: MULTI_NAMESPACE_ISOLATED_TYPE },
];
await bulkCreateSuccess(objects, { namespace });
expectClientCallArgsAction(objects, { method: 'create', getId });
@@ -762,26 +847,30 @@ describe('SavedObjectsRepository', () => {
describe('migration', () => {
it(`migrates the docs and serializes the migrated docs`, async () => {
migrator.migrateDocument.mockImplementation(mockMigrateDocument);
- await bulkCreateSuccess([obj1, obj2]);
- const docs = [obj1, obj2].map((x) => ({ ...x, ...mockTimestampFields }));
+ await bulkCreateSuccess([obj1, obj2, obj3], { overwrite: true });
+ const docs = [obj1, obj2, obj3].map((x) => ({ ...x, ...mockTimestampFields }));
expectMigrationArgs(docs[0], true, 1);
expectMigrationArgs(docs[1], true, 2);
+ expectMigrationArgs(docs[2], true, 3);
const migratedDocs = docs.map((x) => migrator.migrateDocument(x));
expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]);
expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]);
+ expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(3, migratedDocs[2]);
});
it(`adds namespace to body when providing namespace for single-namespace type`, async () => {
- await bulkCreateSuccess([obj1, obj2], { namespace });
+ await bulkCreateSuccess([obj1, obj2, obj3], { namespace });
expectMigrationArgs({ namespace }, true, 1);
expectMigrationArgs({ namespace }, true, 2);
+ expectMigrationArgs({ namespace }, true, 3);
});
it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => {
- await bulkCreateSuccess([obj1, obj2]);
+ await bulkCreateSuccess([obj1, obj2, obj3]);
expectMigrationArgs({ namespace: expect.anything() }, false, 1);
expectMigrationArgs({ namespace: expect.anything() }, false, 2);
+ expectMigrationArgs({ namespace: expect.anything() }, false, 3);
});
it(`doesn't add namespace to body when not using single-namespace type`, async () => {
@@ -795,23 +884,25 @@ describe('SavedObjectsRepository', () => {
});
it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => {
- const objects = [obj1, obj2].map((obj) => ({
+ const objects = [obj1, obj2, obj3].map((obj) => ({
...obj,
type: MULTI_NAMESPACE_ISOLATED_TYPE,
}));
await bulkCreateSuccess(objects, { namespace });
expectMigrationArgs({ namespaces: [namespace] }, true, 1);
expectMigrationArgs({ namespaces: [namespace] }, true, 2);
+ expectMigrationArgs({ namespaces: [namespace] }, true, 3);
});
it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => {
- const objects = [obj1, obj2].map((obj) => ({
+ const objects = [obj1, obj2, obj3].map((obj) => ({
...obj,
type: MULTI_NAMESPACE_ISOLATED_TYPE,
}));
await bulkCreateSuccess(objects);
expectMigrationArgs({ namespaces: ['default'] }, true, 1);
expectMigrationArgs({ namespaces: ['default'] }, true, 2);
+ expectMigrationArgs({ namespaces: ['default'] }, true, 3);
});
it(`doesn't add namespaces to body when not using multi-namespace type`, async () => {
@@ -1121,6 +1212,15 @@ describe('SavedObjectsRepository', () => {
id: 'logstash-*',
attributes: { title: 'Test Two' },
};
+ const obj3 = {
+ type: 'confidential-type',
+ id: 'my-secret',
+ accessControl: {
+ owner: 'alice',
+ },
+ attributes: { title: 'Test Two' },
+ references: [],
+ };
const references = [{ name: 'ref_0', type: 'test', id: '1' }];
const originId = 'some-origin-id';
const namespace = 'foo-namespace';
@@ -1195,7 +1295,7 @@ describe('SavedObjectsRepository', () => {
describe('client calls', () => {
it(`should use the ES bulk action by default`, async () => {
- await bulkUpdateSuccess([obj1, obj2]);
+ await bulkUpdateSuccess([obj1, obj2, obj3]);
expect(client.bulk).toHaveBeenCalled();
});
@@ -1215,8 +1315,8 @@ describe('SavedObjectsRepository', () => {
});
it(`formats the ES request`, async () => {
- await bulkUpdateSuccess([obj1, obj2]);
- const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)];
+ await bulkUpdateSuccess([obj1, obj2, obj3]);
+ const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2), ...expectObjArgs(obj3)];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
@@ -1233,6 +1333,27 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`does not allow the access control specification to be updated`, async () => {
+ await bulkUpdateSuccess([obj3]);
+ const body = [...expectObjArgs(obj3)];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ const [call] = client.bulk.mock.calls;
+ const [payload] = call;
+ const [, object] = payload.body;
+ expect(object.doc).toMatchInlineSnapshot(`
+ Object {
+ "confidential-type": Object {
+ "title": "Test Two",
+ },
+ "references": Array [],
+ "updated_at": "2017-08-14T15:49:14.886Z",
+ }
+ `);
+ });
+
it(`doesnt call Elasticsearch if there are no valid objects to update`, async () => {
const objects = [obj1, obj2].map((x) => ({ ...x, type: 'unknownType' }));
await savedObjectsRepository.bulkUpdate(objects);
@@ -1765,6 +1886,16 @@ describe('SavedObjectsRepository', () => {
expect(client.create.mock.calls[0][0].body.references).toEqual([]);
});
+ it(`accepts an access control specification`, async () => {
+ const test = async (accessControl) => {
+ await createSuccess(type, attributes, { id, accessControl });
+ expect(client.create.mock.calls[0][0].body.accessControl).toEqual(accessControl);
+ client.create.mockClear();
+ };
+ await test({ owner: 'alice' });
+ await test(undefined);
+ });
+
it(`accepts custom references array`, async () => {
const test = async (references) => {
await createSuccess(type, attributes, { id, references });
@@ -2550,7 +2681,7 @@ describe('SavedObjectsRepository', () => {
const generateSearchResults = (namespace) => {
return {
hits: {
- total: 4,
+ total: 5,
hits: [
{
_index: '.kibana',
@@ -2600,6 +2731,23 @@ describe('SavedObjectsRepository', () => {
},
},
},
+ {
+ _index: '.kibana',
+ _id: `${namespace ? `${namespace}:` : ''}confidential-type:my-secret`,
+ _score: 3,
+ ...mockVersionProps,
+ _source: {
+ namespace,
+ type: 'confidential-type',
+ accessControl: {
+ owner: 'alice',
+ },
+ ...mockTimestampFields,
+ 'confidential-type': {
+ name: 'stocks-*',
+ },
+ },
+ },
{
_index: '.kibana',
_id: `${NAMESPACE_AGNOSTIC_TYPE}:something`,
@@ -2816,9 +2964,10 @@ describe('SavedObjectsRepository', () => {
noNamespaceSearchResults.hits.hits.forEach((doc, i) => {
expect(response.saved_objects[i]).toEqual({
- id: doc._id.replace(/(index-pattern|config|globalType)\:/, ''),
+ id: doc._id.replace(/(index-pattern|config|globalType|confidential-type)\:/, ''),
type: doc._source.type,
originId: doc._source.originId,
+ accessControl: doc._source.accessControl,
...mockTimestampFields,
version: mockVersion,
score: doc._score,
@@ -2843,9 +2992,13 @@ describe('SavedObjectsRepository', () => {
namespacedSearchResults.hits.hits.forEach((doc, i) => {
expect(response.saved_objects[i]).toEqual({
- id: doc._id.replace(/(foo-namespace\:)?(index-pattern|config|globalType)\:/, ''),
+ id: doc._id.replace(
+ /(foo-namespace\:)?(index-pattern|config|globalType|confidential-type)\:/,
+ ''
+ ),
type: doc._source.type,
originId: doc._source.originId,
+ accessControl: doc._source.accessControl,
...mockTimestampFields,
version: mockVersion,
score: doc._score,
@@ -3088,8 +3241,14 @@ describe('SavedObjectsRepository', () => {
const id = 'logstash-*';
const namespace = 'foo-namespace';
const originId = 'some-origin-id';
+ const accessControl = { owner: 'alice' };
- const getSuccess = async (type, id, options, includeOriginId) => {
+ const getSuccess = async (
+ type,
+ id,
+ options = {},
+ { includeOriginId = false, includeAccessControl = false } = {}
+ ) => {
const response = getMockGetResponse(
{
type,
@@ -3097,6 +3256,7 @@ describe('SavedObjectsRepository', () => {
// "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the
// operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response.
...(includeOriginId && { originId }),
+ ...(includeAccessControl && { accessControl }),
},
options?.namespace
);
@@ -3246,9 +3406,14 @@ describe('SavedObjectsRepository', () => {
});
it(`includes originId property if present in cluster call response`, async () => {
- const result = await getSuccess(type, id, {}, true);
+ const result = await getSuccess(type, id, {}, { includeOriginId: true });
expect(result).toMatchObject({ originId });
});
+
+ it(`includes accessControl property if present in cluster call response`, async () => {
+ const result = await getSuccess(type, id, {}, { includeAccessControl: true });
+ expect(result).toMatchObject({ accessControl });
+ });
});
});
@@ -3940,6 +4105,23 @@ describe('SavedObjectsRepository', () => {
await test(null);
});
+ it(`doesn't accept an access control specification`, async () => {
+ const accessControlTest = async (accessControl) => {
+ await updateSuccess(type, id, attributes, { accessControl });
+ expect(client.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ body: { doc: expect.not.objectContaining({ accessControl: expect.anything() }) },
+ }),
+ expect.anything()
+ );
+ client.update.mockClear();
+ };
+ await accessControlTest({ owner: 'alice' });
+ await accessControlTest(123);
+ await accessControlTest(true);
+ await accessControlTest(null);
+ });
+
it(`defaults to a refresh setting of wait_for`, async () => {
await updateSuccess(type, id, { foo: 'bar' });
expect(client.update).toHaveBeenCalledWith(
@@ -4018,7 +4200,9 @@ describe('SavedObjectsRepository', () => {
it(`includes _source_includes when type is multi-namespace`, async () => {
await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes);
expect(client.update).toHaveBeenCalledWith(
- expect.objectContaining({ _source_includes: ['namespace', 'namespaces', 'originId'] }),
+ expect.objectContaining({
+ _source_includes: ['namespace', 'namespaces', 'originId', 'accessControl'],
+ }),
expect.anything()
);
});
@@ -4027,7 +4211,7 @@ describe('SavedObjectsRepository', () => {
await updateSuccess(type, id, attributes);
expect(client.update).toHaveBeenLastCalledWith(
expect.objectContaining({
- _source_includes: ['namespace', 'namespaces', 'originId'],
+ _source_includes: ['namespace', 'namespaces', 'originId', 'accessControl'],
}),
expect.anything()
);
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 986467c917dd2..97f1fb5cc5fe8 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -8,6 +8,7 @@
import { omit, isObject } from 'lodash';
import type { estypes } from '@elastic/elasticsearch';
+import { SavedObjectAccessControl } from 'src/core/types';
import {
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
@@ -56,6 +57,8 @@ import {
SavedObjectsRemoveReferencesToOptions,
SavedObjectsRemoveReferencesToResponse,
SavedObjectsResolveResponse,
+ SavedObjectsBulkCreateOptions,
+ SavedObjectsCheckConflictsOptions,
} from '../saved_objects_client';
import {
SavedObject,
@@ -285,6 +288,7 @@ export class SavedObjectsRepository {
originId,
initialNamespaces,
version,
+ accessControl,
} = options;
const namespace = normalizeNamespace(options.namespace);
@@ -316,6 +320,7 @@ export class SavedObjectsRepository {
type,
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
+ ...(accessControl && { accessControl }),
originId,
attributes,
migrationVersion,
@@ -356,7 +361,7 @@ export class SavedObjectsRepository {
*/
async bulkCreate(
objects: Array>,
- options: SavedObjectsCreateOptions = {}
+ options: SavedObjectsBulkCreateOptions = {}
): Promise> {
const { overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options;
const namespace = normalizeNamespace(options.namespace);
@@ -385,6 +390,9 @@ export class SavedObjectsRepository {
const method = id && overwrite ? 'index' : 'create';
const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
+ const requiresAccessControlCheck = false;
+ // object.id && overwrite && this._registry.isPrivate(object.type);
+ const requiresPreflightCheck = requiresNamespacesCheck || requiresAccessControlCheck;
if (id == null) {
object.id = SavedObjectsUtils.generateId();
@@ -395,7 +403,7 @@ export class SavedObjectsRepository {
value: {
method,
object,
- ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }),
+ ...(requiresPreflightCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }),
},
};
});
@@ -406,7 +414,7 @@ export class SavedObjectsRepository {
.map(({ value: { object: { type, id } } }) => ({
_id: this._serializer.generateRawId(namespace, type, id),
_index: this.getIndexForType(type),
- _source: ['type', 'namespaces'],
+ _source: ['type', 'namespaces', 'accessControl'],
}));
const bulkGetResponse = bulkGetDocs.length
? await this.client.mget(
@@ -431,7 +439,7 @@ export class SavedObjectsRepository {
let versionProperties;
const {
esRequestIndex,
- object: { initialNamespaces, version, ...object },
+ object: { initialNamespaces, version, accessControl, ...object },
method,
} = expectedBulkGetResult.value;
if (esRequestIndex !== undefined) {
@@ -453,6 +461,22 @@ export class SavedObjectsRepository {
},
};
}
+ if (docFound && !this.hasCompatibleAccessControl(actualResult, accessControl)) {
+ const { id, type } = object;
+ return {
+ tag: 'Left' as 'Left',
+ error: {
+ id,
+ type,
+ error: {
+ ...errorContent(
+ SavedObjectsErrorHelpers.createIncompatibleAccessControlError(type, id)
+ ),
+ metadata: { isNotOverwritable: true },
+ },
+ },
+ };
+ }
savedObjectNamespaces =
initialNamespaces ||
// @ts-expect-error MultiGetHit._source is optional
@@ -477,6 +501,7 @@ export class SavedObjectsRepository {
type: object.type,
attributes: object.attributes,
migrationVersion: object.migrationVersion,
+ accessControl,
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
updated_at: time,
@@ -539,7 +564,7 @@ export class SavedObjectsRepository {
*/
async checkConflicts(
objects: SavedObjectsCheckConflictsObject[] = [],
- options: SavedObjectsBaseOptions = {}
+ options: SavedObjectsCheckConflictsOptions = {}
): Promise {
if (objects.length === 0) {
return { errors: [] };
@@ -575,7 +600,7 @@ export class SavedObjectsRepository {
const bulkGetDocs = expectedBulkGetResults.filter(isRight).map(({ value: { type, id } }) => ({
_id: this._serializer.generateRawId(namespace, type, id),
_index: this.getIndexForType(type),
- _source: { includes: ['type', 'namespaces'] },
+ _source: { includes: ['type', 'namespaces', 'accessControl'] },
}));
const bulkGetResponse = bulkGetDocs.length
? await this.client.mget(
@@ -598,15 +623,19 @@ export class SavedObjectsRepository {
const { type, id, esRequestIndex } = expectedResult.value;
const doc = bulkGetResponse?.body.docs[esRequestIndex];
if (doc?.found) {
+ const isNotOverwritable =
+ // @ts-expect-error MultiGetHit._source is optional
+ !this.rawDocExistsInNamespace(doc, namespace) ||
+ !this.hasCompatibleAccessControl(doc, options.accessControl);
+
+ const errorMetadata = isNotOverwritable ? { metadata: { isNotOverwritable } } : undefined;
+
errors.push({
id,
type,
error: {
...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)),
- // @ts-expect-error MultiGetHit._source is optional
- ...(!this.rawDocExistsInNamespace(doc!, namespace) && {
- metadata: { isNotOverwritable: true },
- }),
+ ...errorMetadata,
},
});
}
@@ -1245,7 +1274,7 @@ export class SavedObjectsRepository {
doc,
...(rawUpsert && { upsert: rawUpsert._source }),
},
- _source_includes: ['namespace', 'namespaces', 'originId'],
+ _source_includes: ['namespace', 'namespaces', 'originId', 'accessControl'],
require_alias: true,
})
.catch((err) => {
@@ -1256,7 +1285,7 @@ export class SavedObjectsRepository {
throw err;
});
- const { originId } = body.get?._source ?? {};
+ const { originId, accessControl } = body.get?._source ?? {};
let namespaces: string[] = [];
if (!this._registry.isNamespaceAgnostic(type)) {
namespaces = body.get?._source.namespaces ?? [
@@ -1271,6 +1300,7 @@ export class SavedObjectsRepository {
version: encodeHitVersion(body),
namespaces,
...(originId && { originId }),
+ ...(accessControl && { accessControl }),
references,
attributes,
};
@@ -1406,7 +1436,7 @@ export class SavedObjectsRepository {
.map(({ value: { type, id, objectNamespace } }) => ({
_id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id),
_index: this.getIndexForType(type),
- _source: ['type', 'namespaces'],
+ _source: ['type', 'namespaces', 'accessControl'],
}));
const bulkGetResponse = bulkGetDocs.length
? await this.client.mget(
@@ -1526,12 +1556,13 @@ export class SavedObjectsRepository {
const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse;
// eslint-disable-next-line @typescript-eslint/naming-convention
- const { [type]: attributes, references, updated_at } = documentToSave;
+ const { [type]: attributes, references, updated_at, accessControl } = documentToSave;
const { originId } = get._source;
return {
id,
type,
+ accessControl,
...(namespaces && { namespaces }),
...(originId && { originId }),
updated_at,
@@ -2013,6 +2044,19 @@ export class SavedObjectsRepository {
return rawDocExistsInNamespace(this._registry, raw, namespace);
}
+ private hasCompatibleAccessControl(
+ rawDoc:
+ | estypes.GetResponse
+ | estypes.MgetHit
+ | undefined,
+ accessControl: SavedObjectAccessControl | undefined
+ ) {
+ if (!rawDoc?._source?.accessControl) {
+ return true;
+ }
+ return rawDoc._source?.accessControl?.owner === accessControl?.owner;
+ }
+
/**
* Pre-flight check to get a multi-namespace saved object's included namespaces. This ensures that, if the saved object exists, it
* includes the target namespace.
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
index 4df268dafa185..5dcceb3a3908b 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
@@ -601,7 +601,7 @@ describe('#getQueryParams', () => {
expect(
mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
- ).toEqual(['saved.title', 'pending.title', 'saved.desc', 'pending.desc']);
+ ).toEqual(['pending.title', 'saved.title', 'pending.desc', 'saved.desc']);
});
it('uses all registered types when `type` is not provided', () => {
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
index e5b9108795a25..9b5d3473f7a48 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
@@ -17,11 +17,23 @@ import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
* Gets the types based on the type. Uses mappings to support
* null type (all types), a single type string or an array
*/
-function getTypes(registry: ISavedObjectTypeRegistry, type?: string | string[]) {
- if (!type) {
- return registry.getAllTypes().map((registeredType) => registeredType.name);
+function getTypes(
+ registry: ISavedObjectTypeRegistry,
+ typeToNamespacesMap?: Map,
+ allowedTypes?: string | string[]
+) {
+ const allTypes = typeToNamespacesMap
+ ? Array.from(typeToNamespacesMap.keys())
+ : registry.getAllTypes().map((registeredType) => registeredType.name);
+
+ if (Array.isArray(allowedTypes)) {
+ return allTypes.filter((name) => allowedTypes.includes(name));
+ }
+ if (typeof allowedTypes === 'string') {
+ return allTypes.filter((name) => name === allowedTypes);
}
- return Array.isArray(type) ? type : [type];
+ // otherwise, no restrictions
+ return allTypes;
}
/**
@@ -204,10 +216,7 @@ export function getQueryParams({
hasReferenceOperator,
kueryNode,
}: QueryParams) {
- const types = getTypes(
- registry,
- typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type
- );
+ const types = getTypes(registry, typeToNamespacesMap, type);
if (hasReference && !Array.isArray(hasReference)) {
hasReference = [hasReference];
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index abb86d8120a9b..ed8518b739d07 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import type { SavedObjectAccessControl } from 'src/core/types';
import type {
ISavedObjectsRepository,
ISavedObjectsPointInTimeFinder,
@@ -70,8 +71,17 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
+
+ /** The {@link SavedObjectAccessControl | accessControl} to associate with this saved object. */
+ accessControl?: SavedObjectAccessControl;
}
+/**
+ *
+ * @public
+ */
+export type SavedObjectsBulkCreateOptions = Omit;
+
/**
*
* @public
@@ -107,6 +117,9 @@ export interface SavedObjectsBulkCreateObject {
* * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
+
+ /** The {@link SavedObjectAccessControl | accessControl} to associate with this saved object. */
+ accessControl?: SavedObjectAccessControl;
}
/**
@@ -195,6 +208,15 @@ export interface SavedObjectsFindResponse {
pit_id?: string;
}
+/**
+ *
+ * @public
+ */
+export interface SavedObjectsCheckConflictsOptions extends SavedObjectsBaseOptions {
+ /** An {@link SavedObjectAccessControl | accessControl} which should be compatible with conflicting objects. */
+ accessControl?: SavedObjectAccessControl;
+}
+
/**
*
* @public
@@ -423,7 +445,7 @@ export class SavedObjectsClient {
*/
async checkConflicts(
objects: SavedObjectsCheckConflictsObject[] = [],
- options: SavedObjectsBaseOptions = {}
+ options: SavedObjectsCheckConflictsOptions = {}
): Promise {
return await this._repository.checkConflicts(objects, options);
}
diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts
index 1bb214de701e2..2a0704c2f2e8a 100644
--- a/src/core/server/saved_objects/types.ts
+++ b/src/core/server/saved_objects/types.ts
@@ -33,6 +33,7 @@ import { SavedObject } from '../../types';
type KueryNode = any;
export type {
+ SavedObjectAccessControl,
SavedObjectAttributes,
SavedObjectAttribute,
SavedObjectAttributeSingle,
@@ -248,6 +249,13 @@ export type SavedObjectsClientContract = Pick {
* The {@link SavedObjectsNamespaceType | namespace type} for the type.
*/
namespaceType: SavedObjectsNamespaceType;
+
+ /**
+ * The {@link SavedObjectsAccessClassification | access classification} for the type.
+ */
+ accessClassification?: SavedObjectsAccessClassification;
/**
* If defined, the type instances will be stored in the given index instead of the default one.
*/
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 52548c760e30b..7ccd467890462 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -141,6 +141,7 @@ import { Request } from '@hapi/hapi';
import { RequestHandlerContext as RequestHandlerContext_2 } from 'src/core/server';
import { ResponseObject } from '@hapi/hapi';
import { ResponseToolkit } from '@hapi/hapi';
+import { SavedObjectAccessControl as SavedObjectAccessControl_2 } from 'src/core/types';
import { SchemaTypeError } from '@kbn/config-schema';
import { ScrollParams } from 'elasticsearch';
import { SearchParams } from 'elasticsearch';
@@ -2226,6 +2227,8 @@ export type SafeRouteMethod = 'get' | 'options';
//
// @public (undocumented)
export interface SavedObject {
+ // (undocumented)
+ accessControl?: SavedObjectAccessControl;
attributes: T;
coreMigrationVersion?: string;
// Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts
@@ -2242,6 +2245,11 @@ export interface SavedObject {
version?: string;
}
+// @public
+export interface SavedObjectAccessControl {
+ owner: string;
+}
+
// @public
export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttributeSingle[];
@@ -2291,10 +2299,12 @@ export interface SavedObjectReference {
// @public
export interface SavedObjectReferenceWithContext {
+ accessControl?: SavedObjectAccessControl;
id: string;
inboundReferences: Array<{
type: string;
id: string;
+ accessControl?: SavedObjectAccessControl;
name: string;
}>;
isMissing?: boolean;
@@ -2316,6 +2326,7 @@ export interface SavedObjectsBaseOptions {
// @public (undocumented)
export interface SavedObjectsBulkCreateObject {
+ accessControl?: SavedObjectAccessControl_2;
// (undocumented)
attributes: T;
coreMigrationVersion?: string;
@@ -2332,6 +2343,9 @@ export interface SavedObjectsBulkCreateObject {
version?: string;
}
+// @public (undocumented)
+export type SavedObjectsBulkCreateOptions = Omit;
+
// @public (undocumented)
export interface SavedObjectsBulkGetObject {
fields?: string[];
@@ -2380,6 +2394,11 @@ export interface SavedObjectsCheckConflictsObject {
type: string;
}
+// @public (undocumented)
+export interface SavedObjectsCheckConflictsOptions extends SavedObjectsBaseOptions {
+ accessControl?: SavedObjectAccessControl_2;
+}
+
// @public (undocumented)
export interface SavedObjectsCheckConflictsResponse {
// (undocumented)
@@ -2397,7 +2416,7 @@ export class SavedObjectsClient {
bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
- checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsCheckConflictsOptions): Promise;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
@@ -2479,6 +2498,7 @@ export interface SavedObjectsCollectMultiNamespaceReferencesResponse {
// @public (undocumented)
export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
+ accessControl?: SavedObjectAccessControl_2;
coreMigrationVersion?: string;
id?: string;
initialNamespaces?: string[];
@@ -2520,6 +2540,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError;
// (undocumented)
+ static createIncompatibleAccessControlError(type: string, id: string): DecoratedError;
+ // (undocumented)
static createIndexAliasNotFoundError(alias: string): DecoratedError;
// (undocumented)
static createInvalidVersionError(versionInput?: string): DecoratedError;
@@ -2989,10 +3011,10 @@ export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBase
// @public (undocumented)
export class SavedObjectsRepository {
- bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
+ bulkCreate(objects: Array>, options?: SavedObjectsBulkCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
- checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
+ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsCheckConflictsOptions): Promise;
closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise;
collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
@@ -3076,6 +3098,9 @@ export interface SavedObjectStatusMeta {
// @public (undocumented)
export interface SavedObjectsType {
+ // Warning: (ae-forgotten-export) The symbol "SavedObjectsAccessClassification" needs to be exported by the entry point index.d.ts
+ // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "SavedObjectsAccessClassification"
+ accessClassification?: SavedObjectsAccessClassification;
convertToAliasScript?: string;
convertToMultiNamespaceTypeVersion?: string;
hidden: boolean;
@@ -3175,6 +3200,7 @@ export class SavedObjectTypeRegistry {
isImportableAndExportable(type: string): boolean;
isMultiNamespace(type: string): boolean;
isNamespaceAgnostic(type: string): boolean;
+ isPrivate(type: string): boolean;
isShareable(type: string): boolean;
isSingleNamespace(type: string): boolean;
registerType(type: SavedObjectsType): void;
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index 77b5378f9477f..8d84f054b80d4 100644
--- a/src/core/server/types.ts
+++ b/src/core/server/types.ts
@@ -9,6 +9,7 @@
/** This module is intended for consumption by public to avoid import issues with server-side code */
export type { PluginOpaqueId } from './plugins/types';
export type {
+ SavedObjectAccessControl,
SavedObjectsImportResponse,
SavedObjectsImportSuccess,
SavedObjectsImportConflictError,
diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts
index 3a97c2fd6f010..0c98a41f15f33 100644
--- a/src/core/types/saved_objects.ts
+++ b/src/core/types/saved_objects.ts
@@ -47,6 +47,16 @@ export interface SavedObjectReference {
id: string;
}
+/**
+ * The "Access Control" describing which users should be authorized to access this SavedObject.
+ *
+ * @public
+ */
+export interface SavedObjectAccessControl {
+ /** The owner of this SavedObject. */
+ owner: string;
+}
+
/**
* Information about the migrations that have been applied to this SavedObject.
* When Kibana starts up, KibanaMigrator detects outdated documents and
@@ -96,6 +106,7 @@ export interface SavedObject {
* space.
*/
originId?: string;
+ accessControl?: SavedObjectAccessControl;
}
export interface SavedObjectError {
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 781d073b34978..f56adca0b093e 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -100,6 +100,7 @@ import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
import { SavedObject } from 'src/core/server';
import { SavedObject as SavedObject_2 } from 'kibana/server';
+import { SavedObjectAccessControl as SavedObjectAccessControl_2 } from 'src/core/types';
import { SavedObjectReference as SavedObjectReference_2 } from 'src/core/types';
import { SavedObjectsClientContract } from 'src/core/public';
import { SavedObjectsFindOptions } from 'kibana/public';
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index 54fe2a7b0bec2..3ea47b7e65eb0 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -49,6 +49,7 @@ import React from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Request } from '@hapi/hapi';
import * as Rx from 'rxjs';
+import { SavedObjectAccessControl as SavedObjectAccessControl_2 } from 'src/core/types';
import { SavedObjectAttributes } from 'kibana/server';
import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public';
import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public';
diff --git a/x-pack/plugins/security/server/authorization/actions/saved_object.test.ts b/x-pack/plugins/security/server/authorization/actions/saved_object.test.ts
index ceed327b53707..293c9a792cbfd 100644
--- a/x-pack/plugins/security/server/authorization/actions/saved_object.test.ts
+++ b/x-pack/plugins/security/server/authorization/actions/saved_object.test.ts
@@ -35,3 +35,10 @@ describe('#get', () => {
);
});
});
+
+describe(`#manage`, () => {
+ test('returns `saved_object:${version}:manage`', () => {
+ const spaceActions = new SavedObjectActions(version);
+ expect(spaceActions.manage).toBe('saved_object:1.0.0-zeta1:manage');
+ });
+});
diff --git a/x-pack/plugins/security/server/authorization/actions/saved_object.ts b/x-pack/plugins/security/server/authorization/actions/saved_object.ts
index 6d1a23e6f42fe..c2fd1b7e93b56 100644
--- a/x-pack/plugins/security/server/authorization/actions/saved_object.ts
+++ b/x-pack/plugins/security/server/authorization/actions/saved_object.ts
@@ -25,4 +25,8 @@ export class SavedObjectActions {
return `${this.prefix}${type}/${operation}`;
}
+
+ public get manage(): string {
+ return `${this.prefix}manage`;
+ }
}
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
index 5264e74861be1..86149fa9aebce 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
@@ -199,10 +199,11 @@ describe('features', () => {
});
// the `global` and `space` privileges behave very similarly, with the one exception being that
-// "global all" includes the ability to manage spaces. The following tests both groups at once...
+// "global all" includes the ability to manage spaces and saved objects. The following tests both groups at once...
[
{
group: 'global',
+ expectManageSavedObjects: true,
expectManageSpaces: true,
expectGetFeatures: true,
expectEnterpriseSearch: true,
@@ -210,6 +211,7 @@ describe('features', () => {
},
{
group: 'space',
+ expectManageSavedObjects: false,
expectManageSpaces: false,
expectGetFeatures: false,
expectEnterpriseSearch: false,
@@ -218,10 +220,11 @@ describe('features', () => {
].forEach(
({
group,
+ expectManageSavedObjects,
expectManageSpaces,
expectGetFeatures,
- expectEnterpriseSearch,
expectDecryptedTelemetry,
+ expectEnterpriseSearch,
}) => {
describe(`${group}`, () => {
test('actions defined in any feature privilege are included in `all`', () => {
@@ -272,6 +275,7 @@ describe('features', () => {
actions.version,
...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []),
...(expectGetFeatures ? [actions.api.get('features')] : []),
+ ...(expectManageSavedObjects ? [actions.savedObject.manage] : []),
...(expectManageSpaces
? [
actions.space.manage,
@@ -469,7 +473,7 @@ describe('features', () => {
all: ['ignore-me-1', 'ignore-me-2'],
read: ['ignore-me-1', 'ignore-me-2'],
},
- ui: ['ignore-me-1'],
+ ui: [],
},
},
],
@@ -488,6 +492,7 @@ describe('features', () => {
actions.version,
...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []),
...(expectGetFeatures ? [actions.api.get('features')] : []),
+ ...(expectManageSavedObjects ? [actions.savedObject.manage] : []),
...(expectManageSpaces
? [
actions.space.manage,
@@ -554,6 +559,7 @@ describe('features', () => {
actions.version,
...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []),
...(expectGetFeatures ? [actions.api.get('features')] : []),
+ ...(expectManageSavedObjects ? [actions.savedObject.manage] : []),
...(expectManageSpaces
? [
actions.space.manage,
@@ -621,6 +627,7 @@ describe('features', () => {
actions.version,
...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []),
...(expectGetFeatures ? [actions.api.get('features')] : []),
+ ...(expectManageSavedObjects ? [actions.savedObject.manage] : []),
...(expectManageSpaces
? [
actions.space.manage,
@@ -889,6 +896,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1055,6 +1063,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1288,6 +1297,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1427,6 +1437,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1609,6 +1620,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1737,6 +1749,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -1968,6 +1981,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
@@ -2233,6 +2247,7 @@ describe('subFeatures', () => {
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
index c38a5c9a44f57..22b6f264680fe 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
@@ -106,6 +106,7 @@ export function privilegesFactory(
actions.version,
actions.api.get('decryptedTelemetry'),
actions.api.get('features'),
+ actions.savedObject.manage,
actions.space.manage,
actions.ui.get('spaces', 'manage'),
actions.ui.get('management', 'kibana', 'spaces'),
diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts
index 6ce161a898810..296907f137144 100644
--- a/x-pack/plugins/security/server/config.ts
+++ b/x-pack/plugins/security/server/config.ts
@@ -197,6 +197,9 @@ const providersConfigSchema = schema.object(
}
);
+// xpack.security.session.idleTimeout: '1h'
+// xpack.security.session.lifespan: '3d'
+
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
loginAssistanceMessage: schema.string({ defaultValue: '' }),
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 1873ca42324c0..5ca02e0f5bb2b 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -298,6 +298,7 @@ export class SecurityPlugin
authz: this.authorizationSetup,
savedObjects: core.savedObjects,
getSpacesService: () => spaces?.spacesService,
+ getCurrentUser: (request: KibanaRequest) => this.getAuthentication().getCurrentUser(request),
});
defineRoutes({
diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts
index b9ab7cef7f15b..f484e37fdb1ca 100644
--- a/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts
+++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts
@@ -62,6 +62,7 @@ describe('ensureAuthorized', () => {
await ensureAuthorized(deps, types, actions, namespaces);
expect(deps.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith(
[
+ 'saved_object:some-version:manage',
'mock-saved_object:a/foo',
'mock-saved_object:a/bar',
'mock-saved_object:b/foo',
@@ -93,6 +94,8 @@ describe('ensureAuthorized', () => {
['b', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }],
['c', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }],
]),
+ canSpecifyAccessControl: true,
+ requiresObjectAuthorization: false,
};
test('with default options', async () => {
@@ -163,6 +166,8 @@ describe('ensureAuthorized', () => {
['b', { foo: { authorizedSpaces: ['x', 'y'] } }],
['c', { foo: { authorizedSpaces: ['x'] }, bar: { authorizedSpaces: ['x'] } }],
]),
+ canSpecifyAccessControl: false,
+ requiresObjectAuthorization: true,
});
});
});
@@ -208,7 +213,12 @@ describe('ensureAuthorized', () => {
deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges);
const options = { requireFullAuthorization: false };
const result = await ensureAuthorized(deps, types, actions, namespaces, options);
- expect(result).toEqual({ status: 'unauthorized', typeActionMap: new Map() });
+ expect(result).toEqual({
+ status: 'unauthorized',
+ typeActionMap: new Map(),
+ canSpecifyAccessControl: false,
+ requiresObjectAuthorization: true,
+ });
});
});
});
diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts
index c3457e75f9644..6cf38795f5c28 100644
--- a/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts
+++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts
@@ -24,6 +24,8 @@ export interface EnsureAuthorizedOptions {
export interface EnsureAuthorizedResult {
status: 'fully_authorized' | 'partially_authorized' | 'unauthorized';
typeActionMap: Map>;
+ requiresObjectAuthorization: boolean;
+ canSpecifyAccessControl: boolean;
}
export interface EnsureAuthorizedActionResult {
@@ -53,10 +55,13 @@ export async function ensureAuthorized(
actions.map((action) => [deps.actions.savedObject.get(type, action), { type, action }])
)
);
- const privilegeActions = Array.from(privilegeActionsMap.keys());
+ const privilegeActions = [
+ deps.actions.savedObject.manage,
+ ...Array.from(privilegeActionsMap.keys()),
+ ];
const { hasAllRequested, privileges } = await checkPrivileges(deps, privilegeActions, spaceIds);
- const missingPrivileges = getMissingPrivileges(privileges);
+ const missingPrivileges = getMissingPrivileges(privileges, [deps.actions.savedObject.manage]);
const typeActionMap = privileges.kibana.reduce<
Map>
>((acc, { resource, privilege }) => {
@@ -64,7 +69,8 @@ export async function ensureAuthorized(
(resource && missingPrivileges.get(resource)?.has(privilege)) ||
(!resource && missingPrivileges.get(undefined)?.has(privilege));
- if (missingPrivilegesAtResource) {
+ const isManageSavedObjectsPrivilege = privilege === deps.actions.savedObject.manage;
+ if (missingPrivilegesAtResource || isManageSavedObjectsPrivilege) {
return acc;
}
const { type, action } = privilegeActionsMap.get(privilege)!; // always defined
@@ -89,16 +95,36 @@ export async function ensureAuthorized(
});
}, new Map());
- if (hasAllRequested) {
- return { typeActionMap, status: 'fully_authorized' };
+ const isFullyAuthorized = missingPrivileges.size === 0;
+ // `hasAllRequested` comes from the ES privilege check above, which has been augmented to include the "manage" saved object privilege.
+ const requiresObjectAuthorization = hasAllRequested === false;
+ const canSpecifyAccessControl = hasAllRequested === true;
+
+ if (isFullyAuthorized) {
+ return {
+ typeActionMap,
+ status: 'fully_authorized',
+ requiresObjectAuthorization,
+ canSpecifyAccessControl,
+ };
}
if (!requireFullAuthorization) {
const isPartiallyAuthorized = typeActionMap.size > 0;
if (isPartiallyAuthorized) {
- return { typeActionMap, status: 'partially_authorized' };
+ return {
+ typeActionMap,
+ status: 'partially_authorized',
+ requiresObjectAuthorization,
+ canSpecifyAccessControl,
+ };
} else {
- return { typeActionMap, status: 'unauthorized' };
+ return {
+ typeActionMap,
+ status: 'unauthorized',
+ requiresObjectAuthorization,
+ canSpecifyAccessControl,
+ };
}
}
@@ -170,10 +196,13 @@ async function checkPrivileges(
}
}
-function getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) {
+function getMissingPrivileges(
+ privileges: CheckPrivilegesResponse['privileges'],
+ ignorePrivileges: string[] = []
+) {
return privileges.kibana.reduce