Skip to content

Commit ad8aaee

Browse files
committed
fix: LiveQuery role cache is not invalidated for all users affected by a role write or delete
1 parent 715ea9b commit ad8aaee

4 files changed

Lines changed: 114 additions & 6 deletions

File tree

spec/ParseLiveQueryServer.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,6 +1834,88 @@ describe('ParseLiveQueryServer', function () {
18341834
expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined);
18351835
});
18361836

1837+
describe('role cache invalidation', () => {
1838+
const clearCacheChannel = () => `${Parse.applicationId}clearCache`;
1839+
1840+
// The subscriber is mocked, so the handler registered for the clearCache
1841+
// channel is recovered from the spy rather than by publishing for real.
1842+
const clearCacheHandler = server => {
1843+
const call = server.subscriber.subscribe.calls
1844+
.all()
1845+
.find(({ args }) => args[0] === clearCacheChannel());
1846+
return call.args[1];
1847+
};
1848+
1849+
it('publishes a full clear when a role changes without an acting user', () => {
1850+
const controller = new LiveQueryController({ classNames: ['Yolo'] });
1851+
const publish = controller.liveQueryPublisher.parsePublisher.publish;
1852+
1853+
controller.clearCachedRoles(undefined);
1854+
1855+
expect(publish).toHaveBeenCalledTimes(1);
1856+
const [channel, payload] = publish.calls.mostRecent().args;
1857+
expect(channel).toBe(clearCacheChannel());
1858+
expect(JSON.parse(payload)).toEqual({ clearAll: true });
1859+
});
1860+
1861+
it('keeps publishing the user id for older LiveQuery servers', () => {
1862+
const controller = new LiveQueryController({ classNames: ['Yolo'] });
1863+
const publish = controller.liveQueryPublisher.parsePublisher.publish;
1864+
1865+
controller.clearCachedRoles({ id: testUserId });
1866+
1867+
const payload = JSON.parse(publish.calls.mostRecent().args[1]);
1868+
expect(payload).toEqual({ userId: testUserId, clearAll: true });
1869+
});
1870+
1871+
it('clears every cached auth on a full clear message', async () => {
1872+
const parseLiveQueryServer = new ParseLiveQueryServer({});
1873+
const clearAll = spyOn(parseLiveQueryServer, '_clearAllCachedRoles').and.resolveTo();
1874+
const clearOne = spyOn(parseLiveQueryServer, '_clearCachedRoles').and.resolveTo();
1875+
1876+
clearCacheHandler(parseLiveQueryServer)(JSON.stringify({ clearAll: true }));
1877+
1878+
expect(clearAll).toHaveBeenCalledTimes(1);
1879+
expect(clearOne).not.toHaveBeenCalled();
1880+
});
1881+
1882+
it('falls back to the targeted clear when the message has no clearAll', async () => {
1883+
const parseLiveQueryServer = new ParseLiveQueryServer({});
1884+
const clearAll = spyOn(parseLiveQueryServer, '_clearAllCachedRoles').and.resolveTo();
1885+
const clearOne = spyOn(parseLiveQueryServer, '_clearCachedRoles').and.resolveTo();
1886+
1887+
clearCacheHandler(parseLiveQueryServer)(JSON.stringify({ userId: testUserId }));
1888+
1889+
expect(clearOne).toHaveBeenCalledWith(testUserId);
1890+
expect(clearAll).not.toHaveBeenCalled();
1891+
});
1892+
1893+
it('drops the auth cache and the role cache on a full clear', async () => {
1894+
const parseLiveQueryServer = new ParseLiveQueryServer({});
1895+
const roleClear = jasmine.createSpy('clear').and.resolveTo();
1896+
parseLiveQueryServer.cacheController = { role: { clear: roleClear } };
1897+
parseLiveQueryServer.authCache.set('someToken', Promise.resolve({}));
1898+
expect(parseLiveQueryServer.authCache.size).toBe(1);
1899+
1900+
await parseLiveQueryServer._clearAllCachedRoles();
1901+
1902+
expect(parseLiveQueryServer.authCache.size).toBe(0);
1903+
expect(roleClear).toHaveBeenCalledTimes(1);
1904+
});
1905+
1906+
it('survives a role cache that rejects on a full clear', async () => {
1907+
const parseLiveQueryServer = new ParseLiveQueryServer({});
1908+
parseLiveQueryServer.cacheController = {
1909+
role: { clear: () => Promise.reject(new Error('cache down')) },
1910+
};
1911+
parseLiveQueryServer.authCache.set('someToken', Promise.resolve({}));
1912+
1913+
await expectAsync(parseLiveQueryServer._clearAllCachedRoles()).toBeResolved();
1914+
1915+
expect(parseLiveQueryServer.authCache.size).toBe(0);
1916+
});
1917+
});
1918+
18371919
afterEach(function () {
18381920
jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
18391921
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
@@ -2045,4 +2127,5 @@ describe('LiveQueryController', () => {
20452127
original: undefined,
20462128
});
20472129
});
2130+
20482131
});

src/Controllers/LiveQueryController.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export class LiveQueryController {
6161
}
6262

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

src/LiveQuery/ParseCloudCodePublisher.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ class ParseCloudCodePublisher {
2828
this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request);
2929
}
3030

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

src/LiveQuery/ParseLiveQueryServer.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ class ParseLiveQueryServer {
132132
return;
133133
}
134134
if (channel === Parse.applicationId + 'clearCache') {
135-
this._clearCachedRoles(message.userId);
135+
if (message.clearAll) {
136+
this._clearAllCachedRoles();
137+
} else {
138+
// Sent by a Parse Server running an older version, which only
139+
// invalidates the acting user.
140+
this._clearCachedRoles(message.userId);
141+
}
136142
return;
137143
}
138144
this._inflateParseObject(message);
@@ -630,6 +636,20 @@ class ParseLiveQueryServer {
630636
}
631637
}
632638

639+
async _clearAllCachedRoles() {
640+
try {
641+
// Every cached auth holds a flattened role closure, and a role write can
642+
// change the closure of any user, so the whole cache goes. Entries are
643+
// repopulated lazily by getAuthForSessionToken.
644+
this.authCache.clear();
645+
// A standalone LiveQuery server has its own cache controller, which the
646+
// Parse Server that published this message did not clear.
647+
await this.cacheController?.role?.clear();
648+
} catch (e) {
649+
logger.verbose(`Could not clear role cache. ${e}`);
650+
}
651+
}
652+
633653
getAuthForSessionToken(sessionToken?: string): Promise<{ auth?: Auth, userId?: string }> {
634654
if (!sessionToken) {
635655
return Promise.resolve({});

0 commit comments

Comments
 (0)