Skip to content

fix: Deleting a role does not invalidate the cached role closures - #10620

Open
AdrianCurtin wants to merge 2 commits into
parse-community:alphafrom
AdrianCurtin:fix_role_cache_on_delete
Open

fix: Deleting a role does not invalidate the cached role closures#10620
AdrianCurtin wants to merge 2 commits into
parse-community:alphafrom
AdrianCurtin:fix_role_cache_on_delete

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Aug 1, 2026

Copy link
Copy Markdown

Pull Request

Issue

Closes #10619.

Deleting a _Role did not invalidate the role cache, so every user who held that role kept it in their cached closure until the entry expired. ACLs granting access to role:<name> continued to be honored for a role that no longer existed.

cacheController.role.clear() was called from only two places, RestWrite.js:1566 (create and update) and PurgeRouter.js:22. Deletes do not go through RestWrite; they go through rest.js del(), which had no role cache handling at all.

The asymmetry is what makes it a bug rather than a tradeoff: removing a user from a role is a _Role update and clears the cache correctly, while deleting the whole role revokes the same access from every member and cleared nothing.

Approach

Clear the role cache in rest.js del() when className === '_Role', mirroring RestWrite#runDatabaseOperation, including the accompanying liveQueryController.clearCachedRoles call that the delete path was also missing.

Two details worth reviewer attention:

  • Placed after the delete commits, in the .then() following database.destroy, rather than before the write as RestWrite does. Clearing before the write leaves a window in which a concurrent read repopulates the closure from pre-delete state, which would outlive the invalidation.
  • Deliberately outside the hasTriggers || hasLiveQuery || className == '_Session' branch, so it runs for a _Role with no triggers registered. That branch is why the existing cacheAdapter.user.del at rest.js:199 does not run for most classes.
  • Not awaited, matching the existing call site at RestWrite.js:1566. Awaiting would make a Redis blip fail the delete, since RedisCacheAdapter#clear has no internal error handling, and that seemed the worse trade. Happy to change it if maintainers prefer.

The whole role cache is cleared rather than one user's entry, for the same reason RestWrite does it: the cached value is a flattened transitive closure, so deleting a parent role affects the members of every child role, and the delete does not identify which users are affected.

This is independent of #10618 and applies with or without it. With that change merged, role.clear() becomes a scoped SCAN over <appId>:role:* instead of a FLUSHDB, which makes adding this call inexpensive.

Tests

spec/ParseRole.spec.js gains two specs:

  • A deleted role clears the cached closure. This fails on alpha with Expected [ 'role:Doomed' ] to equal null and passes here.
  • Deleting a non-role object leaves the role cache untouched, so the new branch does not over-clear.

The full spec/ParseRole.spec.js (20 specs) and spec/Auth.spec.js (11 specs), the latter being the main consumer of the role cache, both pass against MongoDB 8.

Tasks

  • Add tests
  • Add changes to documentation (code comments)

Summary by CodeRabbit

  • Bug Fixes
    • Deleting a role now immediately clears its cached data.
    • Cached role information for the acting user is invalidated during role deletion.
    • Deleting non-role objects no longer affects role cache entries.

Copilot AI review requested due to automatic review settings August 1, 2026 05:58
@parse-github-assistant

Copy link
Copy Markdown

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant Bot changed the title fix: deleting a role does not invalidate the cached role closures fix: Deleting a role does not invalidate the cached role closures Aug 1, 2026
@parse-github-assistant

Copy link
Copy Markdown

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The REST delete flow now clears the role cache and cached LiveQuery roles when deleting _Role objects. Tests verify asynchronous cache removal for roles and cache preservation for non-role objects.

Changes

Role deletion cache invalidation

Layer / File(s) Summary
Delete-path cache invalidation
src/rest.js, spec/ParseRole.spec.js
The delete flow clears the role cache and cached LiveQuery roles for _Role deletions. Tests verify role-cache removal and preservation during non-role deletion.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Engage In Review Feedback ❌ Error CodeRabbit left CHANGES_REQUESTED feedback on unhandled cache-clear rejections and all-affected LiveQuery invalidation; commit 89b1b6a addressed tests only, with no author discussion or retraction. Respond to each remaining review comment. Implement the requested behavior, or document a technical rationale and obtain explicit reviewer retraction before resolving the comments.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required fix: prefix, starts the change description with a capital letter, and accurately summarizes role cache invalidation on deletion.
Description check ✅ Passed The description follows the template and clearly documents the issue, approach, tests, and completed tasks.
Linked Issues check ✅ Passed The changes satisfy issue #10619 by clearing role and LiveQuery caches after _Role deletion, including unconditional handling and focused tests.
Out of Scope Changes check ✅ Passed The source changes and tests are directly related to fixing _Role cache invalidation and verifying non-role deletion behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Security Check ✅ Passed The change uses an exact _Role check, runs after successful database.destroy, and preserves existing authorization checks; it adds no untrusted input, query, or authentication bypass pattern.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a security-relevant correctness gap in Parse Server’s role caching: deleting a _Role now invalidates cached role closures (and clears LiveQuery’s cached roles), matching the existing behavior on role create/update so access via role:<name> ACLs can’t linger until cache TTL expiry.

Changes:

  • Clear cacheController.role (and liveQueryController.clearCachedRoles) after _Role deletion in rest.js.
  • Add regression coverage to ensure role deletes clear the role cache while non-role deletes do not.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/rest.js Clears role-related caches when deleting a _Role, aligning delete behavior with role writes.
spec/ParseRole.spec.js Adds specs validating role-cache invalidation on role deletion and non-invalidation on other deletes.
Suppressed comments (1)

spec/ParseRole.spec.js:704

  • Like the previous spec, this uses a fixed sleep which can be flaky and slows the suite. Since the behavior you care about is that role cache clearing is not triggered on non-role deletes, you can assert that deterministically by spying on cacheController.role.clear() and verifying it was not called (no timeout needed).
    await object.destroy({ useMasterKey: true });
    await new Promise(resolve => setTimeout(resolve, 200));

    expect(await cacheController.role.get('someUser')).toEqual(['role:Admin']);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/rest.js
Comment on lines +250 to +255
if (className === '_Role') {
config.cacheController.role.clear();
if (config.liveQueryController) {
config.liveQueryController.clearCachedRoles(auth.user);
}
}
Comment thread spec/ParseRole.spec.js
Comment on lines +679 to +693
it('clears the role cache when a role is deleted', async () => {
const cacheController = Parse.Server.cacheController;
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 cacheController.role.put('someUser', ['role:Doomed']);
expect(await cacheController.role.get('someUser')).toEqual(['role:Doomed']);

await role.destroy({ useMasterKey: true });
// The clear is issued without being awaited, matching RestWrite.
await new Promise(resolve => setTimeout(resolve, 200));

expect(await cacheController.role.get('someUser')).toEqual(null);
});

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
spec/ParseRole.spec.js (1)

679-705: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add coverage for LiveQuery role-cache invalidation.

These tests inspect only Parse.Server.cacheController.role. They do not exercise config.liveQueryController.clearCachedRoles(auth.user), which is part of this change. Add a LiveQuery-enabled case or a focused controller spy. Include the master-key deletion path.

🤖 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 `@spec/ParseRole.spec.js` around lines 679 - 705, Extend the role-deletion
cache tests around the existing role destroy case to cover LiveQuery
invalidation through config.liveQueryController.clearCachedRoles(auth.user), not
only cacheController.role. Enable LiveQuery or spy on the controller, verify
clearCachedRoles is invoked for the deleted role’s user, and retain the
useMasterKey deletion path.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@spec/ParseRole.spec.js`:
- Around line 689-690: Replace the fixed 200 ms sleeps in the role-deletion test
around the clear operation with a bounded polling or eventual assertion that
waits until the deleted role is no longer returned from the cache. Assert the
non-role cache immediately after deletion, preserving deterministic timeout
behavior without relying on elapsed time.

In `@src/rest.js`:
- Around line 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.

---

Nitpick comments:
In `@spec/ParseRole.spec.js`:
- Around line 679-705: Extend the role-deletion cache tests around the existing
role destroy case to cover LiveQuery invalidation through
config.liveQueryController.clearCachedRoles(auth.user), not only
cacheController.role. Enable LiveQuery or spy on the controller, verify
clearCachedRoles is invoked for the deleted role’s user, and retain the
useMasterKey deletion path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4301386e-5393-485c-8189-877595d4d466

📥 Commits

Reviewing files that changed from the base of the PR and between 315e157 and 619e7cb.

📒 Files selected for processing (2)
  • spec/ParseRole.spec.js
  • src/rest.js

Comment thread spec/ParseRole.spec.js Outdated
Comment thread src/rest.js
Comment on lines +244 to +255
// 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);
}
}

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.

Refactors ParseRole cache invalidation specs to use the app config controllers instead of global server state, and adds explicit spies for both role cache and LiveQuery role cache clearing. The role-deletion test now waits on the actual clear promise rather than a fixed timeout, making it deterministic, while the non-role deletion test verifies no cache clear methods are called.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleting a _Role does not clear the role cache, so revoked roles stay active for the cache TTL

2 participants