diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index d9b79bc588..dd0610f966 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -836,6 +836,50 @@ describe('ParseLiveQuery', function () { } }); + it('LiveQuery should work with changing role', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['Chat'], + }, + startLiveQueryServer: true, + }); + const user = new Parse.User(); + user.setUsername('username'); + user.setPassword('password'); + await user.signUp(); + + const role = new Parse.Role('Test', new Parse.ACL(user)); + await role.save(); + + const chatQuery = new Parse.Query('Chat'); + const subscription = await chatQuery.subscribe(); + subscription.on('create', () => { + fail('should not call create as user is not part of role.'); + }); + + const object = new Parse.Object('Chat'); + const acl = new Parse.ACL(); + acl.setRoleReadAccess(role, true); + object.setACL(acl); + object.set({ foo: 'bar' }); + await object.save(null, { useMasterKey: true }); + role.getUsers().add(user); + await new Promise(resolve => setTimeout(resolve, 1000)); + await role.save(); + await new Promise(resolve => setTimeout(resolve, 1000)); + object.set('foo', 'yolo'); + await Promise.all([ + new Promise(resolve => { + subscription.on('update', obj => { + expect(obj.get('foo')).toBe('yolo'); + expect(obj.getACL().toJSON()).toEqual({ 'role:Test': { read: true } }); + resolve(); + }); + }), + object.save(null, { useMasterKey: true }), + ]); + }); + it('liveQuery on Session class', async done => { await reconfigureServer({ liveQuery: { classNames: [Parse.Session] }, diff --git a/src/Auth.js b/src/Auth.js index e3196105c7..ce5c71c860 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -230,6 +230,15 @@ Auth.prototype.cacheRoles = function () { return true; }; +Auth.prototype.clearRoleCache = function (sessionToken) { + if (!this.cacheController) { + return false; + } + this.cacheController.role.del(this.user.id); + this.cacheController.user.del(sessionToken); + return true; +}; + Auth.prototype.getRolesByIds = async function (ins) { const results = []; // Build an OR query across all parentRoles diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 064084caa4..9a5b6d0ef1 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -56,6 +56,13 @@ export class LiveQueryController { return false; } + clearCachedRoles(user: any) { + if (!user) { + return; + } + return this.liveQueryPublisher.onClearCachedRoles(user); + } + _makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any { const req = { object: currentObject, diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index 85e95121fb..b84b14ef31 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -19,6 +19,13 @@ class ParseCloudCodePublisher { this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request); } + onClearCachedRoles(user: Parse.Object) { + this.parsePublisher.publish( + Parse.applicationId + 'clearCache', + JSON.stringify({ userId: user.id }) + ); + } + // Request is the request object from cloud code functions. request.object is a ParseObject. _onCloudCodeMessage(type: string, request: any): void { logger.verbose( diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index fa05f23711..3a91797a2f 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,7 +10,13 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers'; +import { + runLiveQueryEventHandlers, + getTrigger, + runTrigger, + resolveError, + toJSONwithObjects, +} from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -71,6 +77,7 @@ class ParseLiveQueryServer { this.subscriber = ParsePubSub.createSubscriber(config); this.subscriber.subscribe(Parse.applicationId + 'afterSave'); this.subscriber.subscribe(Parse.applicationId + 'afterDelete'); + this.subscriber.subscribe(Parse.applicationId + 'clearCache'); // Register message handler for subscriber. When publisher get messages, it will publish message // to the subscribers and the handler will be called. this.subscriber.on('message', (channel, messageStr) => { @@ -82,6 +89,10 @@ class ParseLiveQueryServer { logger.error('unable to parse message', messageStr, e); return; } + if (channel === Parse.applicationId + 'clearCache') { + this._clearCachedRoles(message.userId); + return; + } this._inflateParseObject(message); if (channel === Parse.applicationId + 'afterSave') { this._onAfterSave(message); @@ -468,6 +479,32 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } + async _clearCachedRoles(userId: string) { + try { + const validTokens = await new Parse.Query(Parse.Session) + .equalTo('user', Parse.User.createWithoutData(userId)) + .find({ useMasterKey: true }); + await Promise.all( + validTokens.map(async token => { + const sessionToken = token.get('sessionToken'); + const authPromise = this.authCache.get(sessionToken); + if (!authPromise) { + return; + } + const [auth1, auth2] = await Promise.all([ + authPromise, + getAuthForSessionToken({ cacheController: this.cacheController, sessionToken }), + ]); + auth1.auth?.clearRoleCache(sessionToken); + auth2.auth?.clearRoleCache(sessionToken); + this.authCache.del(sessionToken); + }) + ); + } catch (e) { + logger.verbose(`Could not clear role cache. ${e}`); + } + } + getAuthForSessionToken(sessionToken: ?string): Promise<{ auth: ?Auth, userId: ?string }> { if (!sessionToken) { return Promise.resolve({}); @@ -574,7 +611,6 @@ class ParseLiveQueryServer { if (!acl_has_roles) { return false; } - const roleNames = await auth.getUserRoles(); // Finally, see if any of the user's roles allow them read access for (const role of roleNames) { diff --git a/src/RestWrite.js b/src/RestWrite.js index fd81561732..2be833ad30 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1326,6 +1326,9 @@ RestWrite.prototype.runDatabaseOperation = function () { if (this.className === '_Role') { this.config.cacheController.role.clear(); + if (this.config.liveQueryController) { + this.config.liveQueryController.clearCachedRoles(this.auth.user); + } } if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {