Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions spec/ParseLiveQueryServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,88 @@ describe('ParseLiveQueryServer', function () {
expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined);
});

describe('role cache invalidation', () => {
const clearCacheChannel = () => `${Parse.applicationId}clearCache`;

// The subscriber is mocked, so the handler registered for the clearCache
// channel is recovered from the spy rather than by publishing for real.
const clearCacheHandler = server => {
const call = server.subscriber.subscribe.calls
.all()
.find(({ args }) => args[0] === clearCacheChannel());
return call.args[1];
};

it('publishes a full clear when a role changes without an acting user', () => {
const controller = new LiveQueryController({ classNames: ['Yolo'] });
const publish = controller.liveQueryPublisher.parsePublisher.publish;

controller.clearCachedRoles(undefined);

expect(publish).toHaveBeenCalledTimes(1);
const [channel, payload] = publish.calls.mostRecent().args;
expect(channel).toBe(clearCacheChannel());
expect(JSON.parse(payload)).toEqual({ clearAll: true });
});

it('keeps publishing the user id for older LiveQuery servers', () => {
const controller = new LiveQueryController({ classNames: ['Yolo'] });
const publish = controller.liveQueryPublisher.parsePublisher.publish;

controller.clearCachedRoles({ id: testUserId });

const payload = JSON.parse(publish.calls.mostRecent().args[1]);
expect(payload).toEqual({ userId: testUserId, clearAll: true });
});

it('clears every cached auth on a full clear message', async () => {
const parseLiveQueryServer = new ParseLiveQueryServer({});
const clearAll = spyOn(parseLiveQueryServer, '_clearAllCachedRoles').and.resolveTo();
const clearOne = spyOn(parseLiveQueryServer, '_clearCachedRoles').and.resolveTo();

clearCacheHandler(parseLiveQueryServer)(JSON.stringify({ clearAll: true }));

expect(clearAll).toHaveBeenCalledTimes(1);
expect(clearOne).not.toHaveBeenCalled();
});

it('falls back to the targeted clear when the message has no clearAll', async () => {
const parseLiveQueryServer = new ParseLiveQueryServer({});
const clearAll = spyOn(parseLiveQueryServer, '_clearAllCachedRoles').and.resolveTo();
const clearOne = spyOn(parseLiveQueryServer, '_clearCachedRoles').and.resolveTo();

clearCacheHandler(parseLiveQueryServer)(JSON.stringify({ userId: testUserId }));

expect(clearOne).toHaveBeenCalledWith(testUserId);
expect(clearAll).not.toHaveBeenCalled();
});

it('drops the auth cache and the role cache on a full clear', async () => {
const parseLiveQueryServer = new ParseLiveQueryServer({});
const roleClear = jasmine.createSpy('clear').and.resolveTo();
parseLiveQueryServer.cacheController = { role: { clear: roleClear } };
parseLiveQueryServer.authCache.set('someToken', Promise.resolve({}));
expect(parseLiveQueryServer.authCache.size).toBe(1);

await parseLiveQueryServer._clearAllCachedRoles();

expect(parseLiveQueryServer.authCache.size).toBe(0);
expect(roleClear).toHaveBeenCalledTimes(1);
});

it('survives a role cache that rejects on a full clear', async () => {
const parseLiveQueryServer = new ParseLiveQueryServer({});
parseLiveQueryServer.cacheController = {
role: { clear: () => Promise.reject(new Error('cache down')) },
};
parseLiveQueryServer.authCache.set('someToken', Promise.resolve({}));

await expectAsync(parseLiveQueryServer._clearAllCachedRoles()).toBeResolved();

expect(parseLiveQueryServer.authCache.size).toBe(0);
});
});

afterEach(function () {
jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
Expand Down Expand Up @@ -2115,4 +2197,5 @@ describe('LiveQueryController', () => {
original: undefined,
});
});

});
5 changes: 2 additions & 3 deletions src/Controllers/LiveQueryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ export class LiveQueryController {
}

clearCachedRoles(user: any) {
if (!user) {
return;
}
// Published even without a user. A master key role write or delete carries
// no acting user, and it revokes access just the same.
return this.liveQueryPublisher.onClearCachedRoles(user);
}

Expand Down
10 changes: 8 additions & 2 deletions src/LiveQuery/ParseCloudCodePublisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ class ParseCloudCodePublisher {
this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request);
}

onClearCachedRoles(user: Parse.Object) {
onClearCachedRoles(user: ?Parse.Object) {
// A role write or delete changes the effective role closure of every member
// of that role and of any role inheriting from it, not just the acting
// user, and a master key request has no acting user at all. `clearAll` asks
// subscribers to drop every cached auth. `userId` is still sent when it is
// known so that a LiveQuery server running an older version, which only
// understands the targeted form, keeps behaving as it does today.
this.parsePublisher.publish(
Parse.applicationId + 'clearCache',
JSON.stringify({ userId: user.id })
JSON.stringify({ userId: user?.id, clearAll: true })
);
}

Expand Down
22 changes: 21 additions & 1 deletion src/LiveQuery/ParseLiveQueryServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,13 @@ class ParseLiveQueryServer {
return;
}
if (channel === Parse.applicationId + 'clearCache') {
this._clearCachedRoles(message.userId);
if (message.clearAll) {
this._clearAllCachedRoles();
} else {
// Sent by a Parse Server running an older version, which only
// invalidates the acting user.
this._clearCachedRoles(message.userId);
}
return;
}
this._inflateParseObject(message);
Expand Down Expand Up @@ -661,6 +667,20 @@ class ParseLiveQueryServer {
}
}

async _clearAllCachedRoles() {
try {
// Every cached auth holds a flattened role closure, and a role write can
// change the closure of any user, so the whole cache goes. Entries are
// repopulated lazily by getAuthForSessionToken.
this.authCache.clear();
// A standalone LiveQuery server has its own cache controller, which the
// Parse Server that published this message did not clear.
await this.cacheController?.role?.clear();
} 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