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
40 changes: 40 additions & 0 deletions spec/ParseRole.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
12 changes: 12 additions & 0 deletions src/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Comment on lines +244 to +255

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 '\bclearCachedRoles\b|\bonClearCachedRoles\b|\b_clearCachedRoles\b' src

Repository: parse-community/parse-server

Length of output: 7534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant LiveQuery invalidation methods and the call sites for role deletions.
sed -n '50,70p' src/Controllers/LiveQueryController.js
printf '\n--- ParseLiveQueryServer clear handler ---\n'
sed -n '628,680p' src/LiveQuery/ParseLiveQueryServer.ts
printf '\n--- Publish method ---\n'
sed -n '24,40p' src/LiveQuery/ParseCloudCodePublisher.js
printf '\n--- Rest.js and RestWrite.js role deletion invalidation ---\n'
sed -n '220,265p' src/rest.js
sed -n '1556,1572p' src/RestWrite.js

# Search for other LiveQuery role invalidation paths and authorization cache usage.
printf '\n--- onClearCachedRoles references ---\n'
rg -n -C 3 '\b(onClearCachedRoles|onAfterDelete|_clearCachedRoles|clearCachedRoles)\b' src test

printf '\n--- authCache/sessionToken references in LiveQuery ---\n'
rg -n -C 3 "\b(authCache|sessionToken)\b" src/LiveQuery/ParseLiveQueryServer.ts

Repository: parse-community/parse-server

Length of output: 9104


Invalidate LiveQuery role caches for all affected users.

clearCachedRoles(auth.user) skips LiveQuery invalidation when the request has no user, and clearCachedRoles(this.auth.user) in RestWrite does the same for role writes. LiveQuery publishes one userId, then ParseLiveQueryServer._clearCachedRoles() clears only sessions for that user. Deleting a parent _Role can also revoke a role from children, but the current LiveQuery path does not cover them. Add a role-id/role-orphan invalidation path that targets every affected session.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rest.js` around lines 244 - 255, Update the _Role deletion handling in
RestRequest and the corresponding RestWrite role-write path so LiveQuery
invalidation does not depend on auth.user and reaches every session affected by
the deleted role, including users inheriting it through child roles. Add or
reuse a role-id/role-orphan invalidation mechanism that propagates the deleted
role’s impact and clears all matching cached role sessions, while preserving the
existing global role cache clear.

// Notify LiveQuery server if possible
const perms = schemaController.getClassLevelPermissions(className);
config.liveQueryController.onAfterDelete(className, inflatedObject, null, perms);
Expand Down