Skip to content

Commit

Permalink
fix: live query role cache does not clear when a user is added to a r…
Browse files Browse the repository at this point in the history
…ole (#8026)
  • Loading branch information
dblythy authored Jun 11, 2022
1 parent 0cd902b commit 199dfc1
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 2 deletions.
44 changes: 44 additions & 0 deletions spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] },
Expand Down
9 changes: 9 additions & 0 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/Controllers/LiveQueryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/LiveQuery/ParseCloudCodePublisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
40 changes: 38 additions & 2 deletions src/LiveQuery/ParseLiveQueryServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand All @@ -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);
Expand Down Expand Up @@ -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({});
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down

0 comments on commit 199dfc1

Please sign in to comment.