diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 95e6189a6a..3dc8d134b3 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -675,4 +675,44 @@ describe('Parse Role testing', () => { const fetchedRole = await query.get(savedRole.id, { useMasterKey: true }); expect(fetchedRole.get('name')).toBe('ModifiedName'); }); + + it('clears the role cache when a role is deleted', async () => { + const config = Config.get(Parse.applicationId); + const role = new Parse.Role('Doomed', new Parse.ACL()); + await role.save(null, { useMasterKey: true }); + + // Saving the role already clears the cache, so seed the entry afterwards. + await config.cacheController.role.put('someUser', ['role:Doomed']); + expect(await config.cacheController.role.get('someUser')).toEqual(['role:Doomed']); + + const clearSpy = spyOn(config.cacheController.role, 'clear').and.callThrough(); + const liveQuerySpy = spyOn(config.liveQueryController, 'clearCachedRoles').and.callThrough(); + + await role.destroy({ useMasterKey: true }); + + expect(clearSpy).toHaveBeenCalledTimes(1); + expect(liveQuerySpy).toHaveBeenCalledTimes(1); + // The clear is issued without being awaited, matching RestWrite, so wait on + // the promise the call returned rather than on a fixed delay. + await clearSpy.calls.mostRecent().returnValue; + + expect(await config.cacheController.role.get('someUser')).toEqual(null); + }); + + it('leaves the role cache alone when a non-role object is deleted', async () => { + const config = Config.get(Parse.applicationId); + const object = new Parse.Object('TestObject'); + await object.save(null, { useMasterKey: true }); + + await config.cacheController.role.put('someUser', ['role:Admin']); + + const clearSpy = spyOn(config.cacheController.role, 'clear').and.callThrough(); + const liveQuerySpy = spyOn(config.liveQueryController, 'clearCachedRoles').and.callThrough(); + + await object.destroy({ useMasterKey: true }); + + expect(clearSpy).not.toHaveBeenCalled(); + expect(liveQuerySpy).not.toHaveBeenCalled(); + expect(await config.cacheController.role.get('someUser')).toEqual(['role:Admin']); + }); }); diff --git a/src/rest.js b/src/rest.js index 7a78f2f8b5..60c3f08ba6 100644 --- a/src/rest.js +++ b/src/rest.js @@ -241,6 +241,18 @@ function del(config, auth, className, objectId, context) { ); }) .then(() => { + // A deleted role is revoked from everyone who held it, so the cached role + // closures have to be dropped the same way they are on a role write (see + // RestWrite#runDatabaseOperation). The cached value is a flattened + // transitive closure, so deleting a parent role also affects the members + // of its children, and the whole role cache is cleared rather than one + // user's entry. + if (className === '_Role') { + config.cacheController.role.clear(); + if (config.liveQueryController) { + config.liveQueryController.clearCachedRoles(auth.user); + } + } // Notify LiveQuery server if possible const perms = schemaController.getClassLevelPermissions(className); config.liveQueryController.onAfterDelete(className, inflatedObject, null, perms);