From 15b57dcfa0b0468592033b95f486fbe3dd8c6665 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 30 Oct 2020 17:25:58 -0400 Subject: [PATCH 1/4] Avoid redundant saved objects authorization checks --- ...ecure_saved_objects_client_wrapper.test.ts | 41 +++++---- .../secure_saved_objects_client_wrapper.ts | 84 ++++++++++++------- 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 8136553e4a623..4a76e86802c1f 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -31,7 +31,9 @@ const createSecureSavedObjectsClientWrapperOptions = () => { createBadRequestError: jest.fn().mockImplementation((message) => new Error(message)), isNotFoundError: jest.fn().mockReturnValue(false), } as unknown) as jest.Mocked; - const getSpacesService = jest.fn().mockReturnValue(true); + const getSpacesService = jest.fn().mockReturnValue({ + namespaceToSpaceId: (namespace?: string) => (namespace ? namespace : 'default'), + }); return { actions, @@ -174,7 +176,9 @@ const expectObjectNamespaceFiltering = async ( ); expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith( 'login:', - namespaces.filter((x) => x !== '*') // when we check what namespaces to redact, we don't check privileges for '*', only actual space IDs + ['some-other-namespace'] + // when we check what namespaces to redact, we don't check privileges for '*', only actual space IDs + // we don't check privileges for authorizedNamespace either, as that was already checked earlier in the operation ); }; @@ -206,12 +210,17 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record { describe('#deleteFromNamespaces', () => { const type = 'foo'; const id = `${type}-id`; - const namespace1 = 'foo-namespace'; - const namespace2 = 'bar-namespace'; + const namespace1 = 'default'; + const namespace2 = 'another-namespace'; const namespaces = [namespace1, namespace2]; const privilege = `mock-saved_object:${type}/share_to_space`; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 85e8e21da81b0..ceeb78d07afb4 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -90,9 +90,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { + const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; try { const args = { type, attributes, options }; - const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; await this.ensureAuthorized(type, 'create', namespaces, { args }); } catch (error) { this.auditLogger.log( @@ -113,7 +113,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const savedObject = await this.baseClient.create(type, attributes, options); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, namespaces); } public async checkConflicts( @@ -135,15 +135,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array>, options: SavedObjectsBaseOptions = {} ) { + const namespaces = objects.reduce( + (acc, { initialNamespaces = [] }) => acc.concat(initialNamespaces), + [options.namespace] + ); try { const args = { objects, options }; - const namespaces = objects.reduce( - (acc, { initialNamespaces = [] }) => { - return acc.concat(initialNamespaces); - }, - [options.namespace] - ); - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { args, }); @@ -170,7 +167,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.bulkCreate(objects, options); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, namespaces); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -249,7 +246,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) ); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, options.namespaces ?? [undefined]); } public async bulkGet( @@ -290,7 +287,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) ); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, [options.namespace]); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { @@ -317,7 +314,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra }) ); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } public async update( @@ -348,7 +345,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const savedObject = await this.baseClient.update(type, id, attributes, options); - return await this.redactSavedObjectNamespaces(savedObject); + return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } public async addToNamespaces( @@ -357,9 +354,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsAddToNamespacesOptions = {} ) { + const { namespace } = options; try { const args = { type, id, namespaces, options }; - const { namespace } = options; // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. await this.ensureAuthorized(type, 'share_to_space', namespaces, { args, @@ -395,7 +392,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response); + return await this.redactSavedObjectNamespaces(response, [namespace, ...namespaces]); } public async deleteFromNamespaces( @@ -432,20 +429,20 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response); + return await this.redactSavedObjectNamespaces(response, namespaces); } public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { + const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = [options?.namespace, ...objectNamespaces]; try { - const objectNamespaces = objects - // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; - // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. - .filter(({ namespace }) => namespace !== undefined) - .map(({ namespace }) => namespace!); - const namespaces = [options?.namespace, ...objectNamespaces]; const args = { objects, options }; await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { args, @@ -473,7 +470,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const response = await this.baseClient.bulkUpdate(objects, options); - return await this.redactSavedObjectsNamespaces(response); + return await this.redactSavedObjectsNamespaces(response, namespaces); } private async checkPrivileges( @@ -596,14 +593,21 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return map; } - private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record) { + private redactAndSortNamespaces( + spaceIds: string[], + privilegeMap: Record, + authorizedSpaceIds: string[] + ) { return spaceIds - .map((x) => (x === ALL_SPACES_ID || privilegeMap[x] ? x : UNKNOWN_SPACE)) + .map((x) => + x === ALL_SPACES_ID || privilegeMap[x] || authorizedSpaceIds.includes(x) ? x : UNKNOWN_SPACE + ) .sort(namespaceComparator); } private async redactSavedObjectNamespaces( - savedObject: T + savedObject: T, + authorizedNamespaces: Array ): Promise { if ( this.getSpacesService() === undefined || @@ -613,7 +617,13 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return savedObject; } - const namespaces = savedObject.namespaces.filter((x) => x !== ALL_SPACES_ID); // all users can see the "all spaces" ID + const authorizedSpaceIds = authorizedNamespaces.map((x) => + this.getSpacesService()!.namespaceToSpaceId(x) + ); + // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier + const namespaces = savedObject.namespaces.filter( + (x) => x !== ALL_SPACES_ID && !authorizedSpaceIds.includes(x) + ); if (namespaces.length === 0) { return savedObject; } @@ -622,20 +632,30 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return { ...savedObject, - namespaces: this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + namespaces: this.redactAndSortNamespaces( + savedObject.namespaces, + privilegeMap, + authorizedSpaceIds + ), }; } private async redactSavedObjectsNamespaces( - response: T + response: T, + authorizedNamespaces: Array ): Promise { if (this.getSpacesService() === undefined) { return response; } + + const authorizedSpaceIds = authorizedNamespaces.map((x) => + this.getSpacesService()!.namespaceToSpaceId(x) + ); const { saved_objects: savedObjects } = response; + // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier const namespaces = uniq( savedObjects.flatMap((savedObject) => savedObject.namespaces || []) - ).filter((x) => x !== ALL_SPACES_ID); // all users can see the "all spaces" ID + ).filter((x) => x !== ALL_SPACES_ID && !authorizedSpaceIds.includes(x)); if (namespaces.length === 0) { return response; } @@ -648,7 +668,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ...savedObject, namespaces: savedObject.namespaces && - this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), + this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap, authorizedSpaceIds), })), }; } From bd2300163ed6ec54931161313e33548ed31ca953 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 3 Nov 2020 14:31:50 -0500 Subject: [PATCH 2/4] Changes for review feedback --- ...ecure_saved_objects_client_wrapper.test.ts | 7 +- .../secure_saved_objects_client_wrapper.ts | 76 ++++++++++--------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 51811cc43f2e4..597925f6fd580 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -211,11 +211,8 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record o.type)); } - private async getNamespacesPrivilegeMap(namespaces: string[]) { + private async getNamespacesPrivilegeMap( + namespaces: string[], + previouslypreviouslyAuthorizedSpaceIds: string[] + ) { + const namespacesToCheck = namespaces.filter( + (namespace) => !previouslypreviouslyAuthorizedSpaceIds.includes(namespace) + ); + const initialPrivilegeMap = previouslypreviouslyAuthorizedSpaceIds.reduce( + (acc, spaceId) => acc.set(spaceId, true), + new Map() + ); + if (namespacesToCheck.length === 0) { + return initialPrivilegeMap; + } const action = this.actions.login; - const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); + const checkPrivilegesResult = await this.checkPrivileges(action, namespacesToCheck); // check if the user can log into each namespace - const map = checkPrivilegesResult.privileges.kibana.reduce( - (acc: Record, { resource, authorized }) => { - // there should never be a case where more than one privilege is returned for a given space - // if there is, fail-safe (authorized + unauthorized = unauthorized) - if (resource && (!authorized || !acc.hasOwnProperty(resource))) { - acc[resource] = authorized; - } - return acc; - }, - {} - ); + const map = checkPrivilegesResult.privileges.kibana.reduce((acc, { resource, authorized }) => { + // there should never be a case where more than one privilege is returned for a given space + // if there is, fail-safe (authorized + unauthorized = unauthorized) + if (resource && (!authorized || !acc.hasOwnProperty(resource))) { + acc.set(resource, authorized); + } + return acc; + }, initialPrivilegeMap); return map; } - private redactAndSortNamespaces( - spaceIds: string[], - privilegeMap: Record, - authorizedSpaceIds: string[] - ) { + private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Map) { return spaceIds - .map((x) => - x === ALL_SPACES_ID || privilegeMap[x] || authorizedSpaceIds.includes(x) ? x : UNKNOWN_SPACE - ) + .map((x) => (x === ALL_SPACES_ID || privilegeMap.get(x) ? x : UNKNOWN_SPACE)) .sort(namespaceComparator); } private async redactSavedObjectNamespaces( savedObject: T, - authorizedNamespaces: Array + previouslyAuthorizedNamespaces: Array ): Promise { if ( this.getSpacesService() === undefined || @@ -656,50 +660,52 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return savedObject; } - const authorizedSpaceIds = authorizedNamespaces.map((x) => + const previouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => this.getSpacesService()!.namespaceToSpaceId(x) ); // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier const namespaces = savedObject.namespaces.filter( - (x) => x !== ALL_SPACES_ID && !authorizedSpaceIds.includes(x) + (x) => x !== ALL_SPACES_ID && !previouslyAuthorizedSpaceIds.includes(x) ); if (namespaces.length === 0) { return savedObject; } - const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + const privilegeMap = await this.getNamespacesPrivilegeMap( + namespaces, + previouslyAuthorizedSpaceIds + ); return { ...savedObject, - namespaces: this.redactAndSortNamespaces( - savedObject.namespaces, - privilegeMap, - authorizedSpaceIds - ), + namespaces: this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), }; } private async redactSavedObjectsNamespaces( response: T, - authorizedNamespaces: Array + previouslyAuthorizedNamespaces: Array ): Promise { if (this.getSpacesService() === undefined) { return response; } - const authorizedSpaceIds = authorizedNamespaces.map((x) => + const previouslypreviouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => this.getSpacesService()!.namespaceToSpaceId(x) ); const { saved_objects: savedObjects } = response; // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier const namespaces = uniq( savedObjects.flatMap((savedObject) => savedObject.namespaces || []) - ).filter((x) => x !== ALL_SPACES_ID && !authorizedSpaceIds.includes(x)); + ).filter((x) => x !== ALL_SPACES_ID && !previouslypreviouslyAuthorizedSpaceIds.includes(x)); if (namespaces.length === 0) { return response; } - const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces); + const privilegeMap = await this.getNamespacesPrivilegeMap( + namespaces, + previouslypreviouslyAuthorizedSpaceIds + ); return { ...response, @@ -707,7 +713,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ...savedObject, namespaces: savedObject.namespaces && - this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap, authorizedSpaceIds), + this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap), })), }; } From e97acf0080ec86a8a2ff711f3de6bca1c4d1f1db Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 3 Nov 2020 14:36:57 -0500 Subject: [PATCH 3/4] Remove redundant conditional --- .../saved_objects/secure_saved_objects_client_wrapper.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 266f9c8867d24..d7c12489c1d1c 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -667,9 +667,6 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = savedObject.namespaces.filter( (x) => x !== ALL_SPACES_ID && !previouslyAuthorizedSpaceIds.includes(x) ); - if (namespaces.length === 0) { - return savedObject; - } const privilegeMap = await this.getNamespacesPrivilegeMap( namespaces, @@ -698,9 +695,6 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = uniq( savedObjects.flatMap((savedObject) => savedObject.namespaces || []) ).filter((x) => x !== ALL_SPACES_ID && !previouslypreviouslyAuthorizedSpaceIds.includes(x)); - if (namespaces.length === 0) { - return response; - } const privilegeMap = await this.getNamespacesPrivilegeMap( namespaces, From 1d438eabe71425e80ea8bbc2da8f7c20bcfdbb91 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 3 Nov 2020 23:38:39 -0500 Subject: [PATCH 4/4] More review feedback --- ...ecure_saved_objects_client_wrapper.test.ts | 37 +++++++++++++++++++ .../secure_saved_objects_client_wrapper.ts | 14 +++---- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 597925f6fd580..c6f4ca6dd8afe 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -1161,4 +1161,41 @@ describe('other', () => { test(`assigns errors from constructor to .errors`, () => { expect(client.errors).toBe(clientOpts.errors); }); + + test(`namespace redaction fails safe`, async () => { + const type = 'foo'; + const id = `${type}-id`; + const namespace = 'some-ns'; + const namespaces = ['some-other-namespace', '*', namespace]; + const returnValue = { namespaces, foo: 'bar' }; + clientOpts.baseClient.get.mockReturnValue(returnValue as any); + + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( + getMockCheckPrivilegesSuccess // privilege check for authorization + ); + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + // privilege check for namespace filtering + (_actions: string | string[], _namespaces?: string | string[]) => ({ + hasAllRequested: false, + username: USERNAME, + privileges: { + kibana: [ + // this is a contrived scenario as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // however, in case we do, we should fail-safe (authorized + unauthorized = unauthorized) + { resource: 'some-other-namespace', privilege: 'login:', authorized: false }, + { resource: 'some-other-namespace', privilege: 'login:', authorized: true }, + ], + }, + }) + ); + + const result = await client.get(type, id, { namespace }); + // we will never redact the "All Spaces" ID + expect(result).toEqual(expect.objectContaining({ namespaces: ['*', namespace, '?'] })); + + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); + expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenLastCalledWith('login:', [ + 'some-other-namespace', + ]); + }); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index d7c12489c1d1c..e6e34de4ac9ab 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -616,12 +616,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async getNamespacesPrivilegeMap( namespaces: string[], - previouslypreviouslyAuthorizedSpaceIds: string[] + previouslyAuthorizedSpaceIds: string[] ) { const namespacesToCheck = namespaces.filter( - (namespace) => !previouslypreviouslyAuthorizedSpaceIds.includes(namespace) + (namespace) => !previouslyAuthorizedSpaceIds.includes(namespace) ); - const initialPrivilegeMap = previouslypreviouslyAuthorizedSpaceIds.reduce( + const initialPrivilegeMap = previouslyAuthorizedSpaceIds.reduce( (acc, spaceId) => acc.set(spaceId, true), new Map() ); @@ -634,7 +634,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const map = checkPrivilegesResult.privileges.kibana.reduce((acc, { resource, authorized }) => { // there should never be a case where more than one privilege is returned for a given space // if there is, fail-safe (authorized + unauthorized = unauthorized) - if (resource && (!authorized || !acc.hasOwnProperty(resource))) { + if (resource && (!authorized || !acc.has(resource))) { acc.set(resource, authorized); } return acc; @@ -687,18 +687,18 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return response; } - const previouslypreviouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => + const previouslyAuthorizedSpaceIds = previouslyAuthorizedNamespaces.map((x) => this.getSpacesService()!.namespaceToSpaceId(x) ); const { saved_objects: savedObjects } = response; // all users can see the "all spaces" ID, and we don't need to recheck authorization for any namespaces that we just checked earlier const namespaces = uniq( savedObjects.flatMap((savedObject) => savedObject.namespaces || []) - ).filter((x) => x !== ALL_SPACES_ID && !previouslypreviouslyAuthorizedSpaceIds.includes(x)); + ).filter((x) => x !== ALL_SPACES_ID && !previouslyAuthorizedSpaceIds.includes(x)); const privilegeMap = await this.getNamespacesPrivilegeMap( namespaces, - previouslypreviouslyAuthorizedSpaceIds + previouslyAuthorizedSpaceIds ); return {