diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 5c3e22ac646a..9aa6aca713f0 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -73,7 +73,7 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace, namespaces, originId, workspaces } = _source; + const { type, namespace, namespaces, originId, workspaces, permissions } = _source; const version = _seq_no != null || _primary_term != null @@ -86,6 +86,7 @@ export class SavedObjectsSerializer { ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), ...(originId && { originId }), + ...(permissions && { permissions }), attributes: _source[type], references: _source.references || [], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), @@ -114,6 +115,7 @@ export class SavedObjectsSerializer { version, references, workspaces, + permissions, } = savedObj; const source = { [type]: attributes, @@ -125,6 +127,7 @@ export class SavedObjectsSerializer { ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), ...(workspaces && { workspaces }), + ...(permissions && { permissions }), }; return { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 473a63cf65f4..f4c6569de4fa 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -28,6 +28,7 @@ * under the License. */ +import { Permissions } from '../permission_control/acl'; import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; /** @@ -71,6 +72,7 @@ interface SavedObjectDoc { updated_at?: string; originId?: string; workspaces?: string[]; + permissions?: Permissions; } interface Referencable { 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 9950d0d253bc..fc3b7bfba572 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -173,7 +173,7 @@ describe('SavedObjectsRepository', () => { }); const getMockGetResponse = ( - { type, id, references, namespace: objectNamespace, originId, workspaces }, + { type, id, references, namespace: objectNamespace, originId, workspaces, permissions }, namespace ) => { const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace; @@ -189,6 +189,7 @@ describe('SavedObjectsRepository', () => { ...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }), workspaces, ...(originId && { originId }), + ...(permissions && { permissions }), type, [type]: { title: 'Testing' }, references, @@ -451,24 +452,35 @@ describe('SavedObjectsRepository', () => { }; const namespace = 'foo-namespace'; const workspace = 'foo-workspace'; + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; 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, permissions }) => ({ + create: { + _id: `${namespace ? `${namespace}:` : ''}${type}:${id}`, + _source: { + [type]: attributes, + type, + namespace, + ...(originId && { originId }), + ...(permissions && { permissions }), + references, + ...mockTimestampFields, + migrationVersion: migrationVersion || { [type]: '1.1.1' }, + }, + ...mockVersionProps, }, - ...mockVersionProps, - }, - })), + }) + ), }; }; @@ -750,6 +762,18 @@ describe('SavedObjectsRepository', () => { expect.anything() ); }); + + it(`accepts permissions property when providing permissions info`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, permissions: permissions })); + await bulkCreateSuccess(objects); + const expected = expect.objectContaining({ permissions }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }); }); describe('errors', () => { @@ -1088,6 +1112,17 @@ describe('SavedObjectsRepository', () => { ); expect(result.saved_objects[1].id).toEqual(obj2.id); }); + + it(`includes permissions property if present`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, permissions: permissions })); + const result = await bulkCreateSuccess(objects); + expect(result).toEqual({ + saved_objects: [ + expect.objectContaining({ permissions }), + expect.objectContaining({ permissions }), + ], + }); + }); }); }); @@ -1307,6 +1342,22 @@ describe('SavedObjectsRepository', () => { ], }); }); + + it(`includes permissions property if present`, async () => { + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; + const obj = { id: 'three', type: MULTI_NAMESPACE_TYPE, permissions: permissions }; + const result = await bulkGetSuccess([obj]); + expect(result).toEqual({ + saved_objects: [expect.objectContaining({ permissions: permissions })], + }); + }); }); }); @@ -1324,6 +1375,14 @@ describe('SavedObjectsRepository', () => { const references = [{ name: 'ref_0', type: 'test', id: '1' }]; const originId = 'some-origin-id'; const namespace = 'foo-namespace'; + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; const getMockBulkUpdateResponse = (objects, options, includeOriginId) => ({ items: objects.map(({ type, id }) => ({ @@ -1584,6 +1643,20 @@ describe('SavedObjectsRepository', () => { await bulkUpdateSuccess([{ ..._obj2, namespace }]); expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); }); + + it(`accepts permissions property when providing permissions info`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, permissions: permissions })); + await bulkUpdateSuccess(objects); + const doc = { + doc: expect.objectContaining({ permissions }), + }; + const body = [expect.any(Object), doc, expect.any(Object), doc]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }); }); describe('errors', () => { @@ -1776,6 +1849,14 @@ describe('SavedObjectsRepository', () => { ], }); }); + + it(`includes permissions property if present`, async () => { + const obj = { type: MULTI_NAMESPACE_TYPE, id: 'three', permissions: permissions }; + const result = await bulkUpdateSuccess([obj1, obj], {}, true); + expect(result).toEqual({ + saved_objects: [expect.anything(), expect.objectContaining({ permissions })], + }); + }); }); }); @@ -1965,6 +2046,14 @@ describe('SavedObjectsRepository', () => { id: '123', }, ]; + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; const createSuccess = async (type, attributes, options) => { const result = await savedObjectsRepository.create(type, attributes, options); @@ -2193,6 +2282,16 @@ describe('SavedObjectsRepository', () => { expect.anything() ); }); + + it(`accepts permissions property`, async () => { + await createSuccess(type, attributes, { id, permissions }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ permissions }), + }), + expect.anything() + ); + }); }); describe('errors', () => { @@ -2288,6 +2387,11 @@ describe('SavedObjectsRepository', () => { expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); + it(`adds permissions to body when providing permissions info`, async () => { + await createSuccess(type, attributes, { id, permissions }); + expectMigrationArgs({ permissions }); + }); + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { await createSuccess(type, attributes, { id, namespace }); expectMigrationArgs({ namespace }); @@ -2334,11 +2438,13 @@ describe('SavedObjectsRepository', () => { namespace, references, originId, + permissions, }); expect(result).toEqual({ type, id, originId, + permissions, ...mockTimestampFields, version: mockVersion, attributes, @@ -3208,7 +3314,7 @@ describe('SavedObjectsRepository', () => { const namespace = 'foo-namespace'; const originId = 'some-origin-id'; - const getSuccess = async (type, id, options, includeOriginId) => { + const getSuccess = async (type, id, options, includeOriginId, permissions) => { const response = getMockGetResponse( { type, @@ -3216,6 +3322,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 }), + ...(permissions && { permissions }), }, options?.namespace ); @@ -3366,6 +3473,21 @@ describe('SavedObjectsRepository', () => { const result = await getSuccess(type, id, {}, true); expect(result).toMatchObject({ originId }); }); + + it(`includes permissions property if present`, async () => { + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; + const result = await getSuccess(type, id, { namespace }, undefined, permissions); + expect(result).toMatchObject({ + permissions: permissions, + }); + }); }); }); @@ -3967,6 +4089,14 @@ describe('SavedObjectsRepository', () => { }, ]; const originId = 'some-origin-id'; + const permissions = { + read: { + users: ['user1'], + }, + write: { + groups: ['groups1'], + }, + }; const updateSuccess = async (type, id, attributes, options, includeOriginId) => { if (registry.isMultiNamespace(type)) { @@ -4143,6 +4273,18 @@ describe('SavedObjectsRepository', () => { expect.anything() ); }); + + it(`accepts permissions when providing permissions info`, async () => { + await updateSuccess(type, id, attributes, { permissions }); + const expected = expect.objectContaining({ permissions }); + const body = { + doc: expected, + }; + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); }); describe('errors', () => { @@ -4237,6 +4379,11 @@ describe('SavedObjectsRepository', () => { const result = await updateSuccess(type, id, attributes, {}, true); expect(result).toMatchObject({ originId }); }); + + it(`includes permissions property if present`, async () => { + const result = await updateSuccess(type, id, attributes, { permissions }); + expect(result).toMatchObject({ permissions }); + }); }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index d45bac5cf836..d38b1e31811a 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -245,6 +245,7 @@ export class SavedObjectsRepository { initialNamespaces, version, workspaces, + permissions, } = options; const namespace = normalizeNamespace(options.namespace); @@ -315,6 +316,7 @@ export class SavedObjectsRepository { updated_at: time, ...(Array.isArray(references) && { references }), ...(Array.isArray(savedObjectWorkspaces) && { workspaces: savedObjectWorkspaces }), + ...(permissions && { permissions }), }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); @@ -536,6 +538,7 @@ export class SavedObjectsRepository { references: object.references || [], originId: object.originId, workspaces: savedObjectWorkspaces, + ...(object.permissions && { permissions: object.permissions }), }) as SavedObjectSanitizedDoc ), }; @@ -1122,7 +1125,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { originId, updated_at: updatedAt, workspaces } = body._source; + const { originId, updated_at: updatedAt, workspaces, permissions } = body._source; let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { @@ -1138,6 +1141,7 @@ export class SavedObjectsRepository { ...(originId && { originId }), ...(updatedAt && { updated_at: updatedAt }), ...(workspaces && { workspaces }), + ...(permissions && { permissions }), version: encodeHitVersion(body), attributes: body._source[type], references: body._source.references || [], @@ -1166,7 +1170,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { version, references, refresh = DEFAULT_REFRESH_SETTING } = options; + const { version, references, refresh = DEFAULT_REFRESH_SETTING, permissions } = options; const namespace = normalizeNamespace(options.namespace); let preflightResult: SavedObjectsRawDoc | undefined; @@ -1180,6 +1184,7 @@ export class SavedObjectsRepository { [type]: attributes, updated_at: time, ...(Array.isArray(references) && { references }), + ...(permissions && { permissions }), }; const { body, statusCode } = await this.client.update( @@ -1218,6 +1223,7 @@ export class SavedObjectsRepository { namespaces, ...(originId && { originId }), ...(workspaces && { workspaces }), + ...(permissions && { permissions }), references, attributes, }; @@ -1418,7 +1424,7 @@ export class SavedObjectsRepository { }; } - const { attributes, references, version, namespace: objectNamespace } = object; + const { attributes, references, version, namespace: objectNamespace, permissions } = object; if (objectNamespace === ALL_NAMESPACES_STRING) { return { @@ -1439,6 +1445,7 @@ export class SavedObjectsRepository { [type]: attributes, updated_at: time, ...(Array.isArray(references) && { references }), + ...(permissions && { permissions }), }; const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type); @@ -1591,7 +1598,7 @@ export class SavedObjectsRepository { )[0] as any; // eslint-disable-next-line @typescript-eslint/naming-convention - const { [type]: attributes, references, updated_at } = documentToSave; + const { [type]: attributes, references, updated_at, permissions } = documentToSave; if (error) { return { id, @@ -1611,6 +1618,7 @@ export class SavedObjectsRepository { version: encodeVersion(seqNo, primaryTerm), attributes, references, + ...(permissions && { permissions }), }; }), }; @@ -1903,7 +1911,7 @@ function getSavedObjectFromSource( id: string, doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } ): SavedObject { - const { originId, updated_at: updatedAt, workspaces } = doc._source; + const { originId, updated_at: updatedAt, workspaces, permissions } = doc._source; let namespaces: string[] = []; if (!registry.isNamespaceAgnostic(type)) { @@ -1923,6 +1931,7 @@ function getSavedObjectFromSource( attributes: doc._source[type], references: doc._source.references || [], migrationVersion: doc._source.migrationVersion, + ...(permissions && { permissions }), }; } 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 732b566d22db..0f2aaac4db1e 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -39,6 +39,7 @@ import { SavedObjectsFindOptions, } from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; +import { Permissions } from '../permission_control/acl'; /** * @@ -68,6 +69,8 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * Note: this can only be used for multi-namespace object types. */ initialNamespaces?: string[]; + /** permission control describe by ACL object */ + permissions?: Permissions; } /** @@ -98,7 +101,7 @@ export interface SavedObjectsBulkCreateObject { * @public */ export interface SavedObjectsBulkUpdateObject - extends Pick { + extends Pick { /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ id: string; /** The type of this Saved Object. Each plugin can define it's own custom Saved Object types. */ @@ -180,6 +183,8 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; /** The OpenSearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; + /** permission control describe by ACL object */ + permissions?: Permissions; } /** diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index fa4f5ab97fdf..9c14aeca98bb 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -27,6 +27,7 @@ * specific language governing permissions and limitations * under the License. */ +import { Permissions } from '../server/saved_objects/permission_control/acl'; /** * Don't use this type, it's simply a helper type for {@link SavedObjectAttribute} @@ -115,6 +116,8 @@ export interface SavedObject { originId?: string; /** Workspaces that this saved object exists in. */ workspaces?: string[]; + /** ACL description of this saved object */ + permissions?: Permissions; } export interface SavedObjectError {