v5.7.0 - Keyspaced Cache, Client-Owned Authorization, Access Decisions - #32
Conversation
9cccbf3 to
0ed8a35
Compare
There was a problem hiding this comment.
🟡 Not ready to approve
A few concrete issues in newly added cache code (missing require dependencies and misleading error/documentation text) should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR delivers the 5.7.0 release feature set, centered on safer, app-scoped caching and per-client authorization state, plus supporting test-stack wiring (Redis-backed Parse Server cache) and documentation updates.
Changes:
- Introduces a reserved, app-scoped cache keyspace (
Parse::Cache::Keyspace) and immutable per-client cache views (Parse::Cache::ScopedView) to prevent cross-app key collisions and unsafe clears. - Refactors identity/role resolution into
Parse::Authorization(one context perParse::Client), adds webhook-driven cache invalidation, and hardens mongo-direct usage with client/connection binding checks. - Adds an optional, read-only consumer of Parse Server’s own role cache (
Parse::Cache::UpstreamRoles) and a shared-database isolation probe, plus updates the Docker integration stack to expose Parse Server’s cache via Redis.
File summaries
| File | Description |
|---|---|
| test/lib/parse/webhook_non_object_triggers_test.rb | Adds coverage that non-object after_* webhook routes compose rather than replace. |
| test/lib/parse/webhook_afterfind_integration_test.rb | Updates Parse Server version reference in afterFind integration test comment. |
| test/lib/parse/vector_visibility_test.rb | Updates Parse Server version reference in vector visibility test comment. |
| test/lib/parse/server_capabilities_test.rb | Updates pinned “current server” tests from 9.9 to 9.10. |
| test/lib/parse/query/explain_public_warn_test.rb | Updates version used in warning tests from 9.9 to 9.10. |
| test/lib/parse/mongodb_role_graph_test.rb | Adjusts role graph depth ceiling tests to 10. |
| test/lib/parse/mongodb_client_binding_test.rb | New tests for mongo-direct client/application binding guard. |
| test/lib/parse/cache_upstream_roles_test.rb | New unit tests for reading/validating Parse Server’s upstream role cache entries. |
| test/lib/parse/cache_upstream_roles_integration_test.rb | New end-to-end tests validating upstream role cache reads against a live Parse Server + Redis cache adapter. |
| test/lib/parse/cache_sub_cache_test.rb | New unit tests for identity/role “planes” (SubCache) behavior and generations/expiry. |
| test/lib/parse/cache_scoped_view_test.rb | New tests for immutable scoped cache views and cross-client isolation. |
| test/lib/parse/cache_keyspace_middleware_test.rb | New tests for keyspace-driven middleware key layout + eviction behavior. |
| test/lib/parse/cache_invalidation_test.rb | New tests verifying webhook-triggered identity/role cache invalidation. |
| test/lib/parse/atlas_search/session_test.rb | Reworks tests to validate AtlasSearch session shim delegates into Authorization context. |
| test/cloud/redis-cache-adapter.js | Adds a test-only parse-server Redis cache adapter module with safety checks. |
| scripts/start-parse.sh | Wires in the test-only Redis cache adapter when PARSE_CACHE_REDIS_URL is present. |
| scripts/docker/Dockerfile.parse | Bumps parse-server image pin to 9.10.0. |
| scripts/docker/docker-compose.test.yml | Adds Redis healthcheck dependency and configures Parse Server cache adapter env. |
| README.md | Adds “What’s new in 5.7” + expands caching documentation and usage examples. |
| lib/parse/webhooks.rb | Updates webhook route registration to compose non-rejectable non-object after_* handlers. |
| lib/parse/stack/version.rb | Bumps gem version to 5.7.0. |
| lib/parse/mongodb.rb | Adds client/application binding guard for process-global MongoDB connection; adjusts role graph defaults/caps. |
| lib/parse/client/caching.rb | Adds keyspace-aware cache keys, auth discrimination, resource-wide eviction via patterns, and broadened Redis error handling. |
| lib/parse/client.rb | Installs keyspace + scoped cache view, registers invalidation hooks, and adds per-client authorization context. |
| lib/parse/cache/upstream_roles.rb | Adds UpstreamRoles reader for Parse Server role cache entries (read-only, fail-closed). |
| lib/parse/cache/sub_cache.rb | Adds SubCache planes with generation counters and epochs for invalidation/freshness. |
| lib/parse/cache/scoped_view.rb | Adds ScopedView and KeyspacedStore to prevent cross-client keyspace rebinding and unsafe clears. |
| lib/parse/cache/redis.rb | Adds scoped views, upstream-cache attachment and isolation verification, and SCAN+UNLINK deletion instrumentation. |
| lib/parse/cache/keyspace.rb | Adds Keyspace owner for cache key layout and clear patterns with glob-safe scoping. |
| lib/parse/cache/invalidation.rb | Adds webhook-trigger registration to invalidate role and identity planes. |
| lib/parse/authorization.rb | Adds per-client Authorization Context with identity/role caches and optional upstream comparison instrumentation. |
| lib/parse/atlas_search/session.rb | Replaces AtlasSearch session resolver with a compatibility shim over Authorization. |
| lib/parse/atlas_search.rb | Delegates legacy AtlasSearch cache knobs into the default client’s Authorization context. |
| lib/parse/acl_scope.rb | Switches ACL scope resolution to Authorization contexts and threads client identity through resolutions. |
| Gemfile.lock | Updates gem version and dependency lock updates (incl. redis 6.0.0). |
| .gitignore | Ensures docs/caching.md is included in repo. |
| .env.test | Adds Redis env vars for both SDK cache and Parse Server cache adapter. |
Review details
Suppressed comments (1)
lib/parse/cache/scoped_view.rb:303
- The UnscopedClearRefused error message also points callers at
client.cache.store.clear, butstoreis a writer method, not an accessor. The remedy should referencewrapped(or otherwise describe how to intentionally invoke the underlying store's unscoped clear).
"under #{@keyspace.root_prefix}. Refusing rather than falling back to an " \
"unscoped clear, which on a Redis-backed store is FLUSHDB and would delete " \
"other applications' entries and any parse-stack:foc:v1:* create-locks. " \
"Use Parse::Cache::Redis for scoped clearing, or call " \
"client.cache.store.clear to take the unscoped clear deliberately."
- Files reviewed: 38/41 changed files
- Comments generated: 3
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
New code includes concrete correctness issues (missing required dependencies in UpstreamRoles, and operator guidance/error text that references a nonexistent/incorrect remedy) that should be fixed before release.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
lib/parse/cache/upstream_roles.rb:6
Parse::Cache::UpstreamRolesusesJSON.parseandSetbut does not requirejsonorset, so requiring this file directly can raiseNameError(Set) or fail to resolve JSON depending on load order. Add explicit requires to make the file self-contained.
require "digest"
require "json"
require "set"
lib/parse/cache/scoped_view.rb:286
- The YARD doc suggests calling
client.cache.store.clear, butstoreis the Moneta writer method on this wrapper, not an accessor to the wrapped backend. This guidance is misleading; it should point atwrapped(the explicit accessor provided).
# @raise [Parse::Cache::UnscopedClearRefused] when the wrapped store
# cannot enumerate its keys, and therefore cannot clear within a
# keyspace. Use a {Parse::Cache::Redis} for scoped clearing, or call
# `client.cache.wrapped.clear` to take the unscoped clear deliberately.
# @return [self]
lib/parse/cache/scoped_view.rb:303
- The raised
UnscopedClearRefusedmessage tells operators to callclient.cache.store.clear, butstoreis the write method and won’t give access to the underlying store. This should referenceclient.cache.wrapped.clear(the actual accessor) so the remedy is actionable.
raise UnscopedClearRefused,
"#{@wrapped.class} cannot enumerate its keys, so it cannot clear only the entries " \
"under #{@keyspace.root_prefix}. Refusing rather than falling back to an " \
"unscoped clear, which on a Redis-backed store is FLUSHDB and would delete " \
"other applications' entries and any parse-stack:foc:v1:* create-locks. " \
"Use Parse::Cache::Redis for scoped clearing, or call " \
"client.cache.wrapped.clear to take the unscoped clear deliberately."
- Files reviewed: 38/41 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Human review recommended
It introduces broad, security-sensitive changes to cache keying/clearing and authorization boundaries across multiple subsystems, requiring careful human validation beyond the specific issues noted.
Review details
Suppressed comments (2)
lib/parse/cache/redis.rb:603
delete_keys_matching!reportsdeletedaskeys.sizeper SCAN batch, which can overcount if keys disappear between SCAN and UNLINK/DEL (or if the command returns a smaller deleted count). Since this value is emitted inparse.cache.evict, it should use the command’s return value to keep instrumentation accurate.
return nil if raw.nil?
JSON.parse(raw)
rescue JSON::ParserError, EncodingError, TypeError
# ParserError covers malformed and hostile-depth JSON
# (JSON::NestingError subclasses it); TypeError covers a
lib/parse/mongodb.rb:449
@bound_application_id = nilis mis-indented relative to the surrounding assignments inreset!, which looks accidental and makes the method harder to read/scan.
@bound_application_id = nil
- Files reviewed: 38/41 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Human review recommended
It makes broad, security- and correctness-sensitive changes across caching, authorization, mongo-direct enforcement, and operational Redis behavior that warrant careful human validation beyond automated review.
Review details
Suppressed comments (4)
lib/parse/cache/invalidation.rb:87
- Indentation in this
guard do/caseblock is inconsistent (thecaseand its comments are aligned withguard do), making the nesting hard to follow and likely violating Ruby style checks. Indent the block so its structure is clear.
Parse::Webhooks.route(type, class_name) do |payload|
guard do
case type
when :after_logout
# The only trigger Parse Server permits on `_Session`. The
lib/parse/cache/invalidation.rb:107
- Continuation of the indentation issue: the body of the
when :after_logoutbranch and theelsebranch should be indented under thecasestatement, and the closingends should align with their corresponding blocks.
token = payload.respond_to?(:session_token) ? payload.session_token : nil
if token && !token.to_s.empty?
cache.identity.invalidate(token.to_s)
else
bump_subject(cache, subject_id(payload))
lib/parse/cache/invalidation.rb:76
- The
guard doblock body is not indented, which makes the control flow hard to read and is likely to violate Ruby style/linting rules (the block contents should be nested underguard do).
This issue also appears in the following locations of the same file:
- line 83
- line 103
Parse::Webhooks.route(type, class_name) do |payload|
guard do
# A role write does not say which users are affected: membership
# and hierarchy changes arrive as relation deltas on `users` and
# `roles`, and the cached value is a flattened transitive closure,
lib/parse/mongodb.rb:449
- Indentation in
reset!is inconsistent (@bound_application_id = nilis misaligned), which makes this method harder to scan and can violate style/lint rules.
@client&.close rescue nil
@client = nil
@enabled = false
@uri = nil
@bound_application_id = nil
- Files reviewed: 38/41 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
Parse::Cache::ScopedView#identity/#roles default TTLs to nil, which can make generation keys permanent and role cache entries never expire if the plane is first initialized without an explicit TTL.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
lib/parse/cache/scoped_view.rb:178
rolesdefaultsttl:to nil, so a caller that omitsttl:ends up with role-closure entries that never expire. That can lead to stale authorization decisions lasting indefinitely on a shared Redis plane.
Set a conservative default TTL (matching Parse::Authorization::Context::DEFAULT_ROLE_TTL) so the safe behavior holds even when ttl: isn’t passed.
# @param ttl [Integer, nil]
# @return [Parse::Cache::SubCache]
def roles(ttl: nil)
@roles ||= Parse::Cache::SubCache.new(store: self, keyspace: @keyspace, family: :role, ttl: ttl)
end
lib/parse/cache/scoped_view.rb:172
identitydefaultsttl:to nil, which creates a plane with no default TTL. If this is the first call (e.g. via webhook invalidation before the app configuresParse::Authorization),SubCache#generation_ttlbecomes nil and generation keys can become permanent, reintroducing unbounded Redis growth.
Give identity a safe default TTL (matching Parse::Authorization::Context::DEFAULT_IDENTITY_TTL), so generation keys always get an expiry even when the caller omits ttl:.
This issue also appears on line 174 of the same file.
# @param ttl [Integer, nil]
# @return [Parse::Cache::SubCache]
def identity(ttl: nil)
@identity ||= Parse::Cache::SubCache.new(store: self, keyspace: @keyspace, family: :idn, ttl: ttl)
end
CHANGELOG.md:40
- This changelog entry attributes
family:/tenant:narrowing anddelete_matchingeviction toParse::Cache::Redis, but in this release those behaviors are implemented on the keyspace-bound scoped view (Parse::Cache::ScopedView/client.cache). On the backend (Parse::Cache::Redis)clear(family:/tenant:)raises anddelete_matchingis a no-op.
Update the wording to point readers at the scoped view API to avoid confusing or broken copy/paste in upgrades.
- **NEW**: `Parse::Cache::Redis#clear` accepts `family:` and `tenant:` to
narrow a clear to one key family or one cache tenant, and
`#delete_matching(pattern)` evicts by glob. A pattern outside the wrapper's
own keyspace is a no-op rather than an unscoped scan, so the narrower API
cannot become a back door to the blast radius the keyspace exists to close.
- Files reviewed: 38/41 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
Parse::Cache::ScopedView#identity/#roles can be memoized with ttl=nil via invalidation hook installation, which defeats generation expiry/TTL clamping and can reintroduce unbounded growth.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
- Files reviewed: 39/42 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
The generation counter expiry refresh in Parse::Cache::SubCache can overwrite concurrent bumps (counter can move backwards), which risks re-admitting stale authorization state and should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
lib/parse/mongodb.rb:494
- Indentation regression:
@bound_app_scope = nilis misindented relative to the other assignments inreset!, which makes the method harder to read and is likely accidental.
@bound_app_scope = nil
- Files reviewed: 40/43 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Raise MongoDB role graph depth defaults and limits from 6 to 10 so the mongo fast path matches `Parse::Role.all_for_user` default behavior. This avoids unexpected `ArgumentError` when callers use the default depth, while keeping traversal safety controls in place via the existing max-time budget. Updated role graph tests to assert the new cap, rejection threshold, and `$graphLookup` depth mapping.
Updates the pinned parse-server version from 9.9.0 to 9.10.0 in the Dockerfile and updates all test fixtures, comments, and capability test names that referenced the old version string.
Introduces Parse::Cache::Keyspace with a reserved key layout (parse-stack:v1:<app_scope>:<ns>:<family>:...) so key generation and scoped eviction can never drift apart. Fixes FLUSHDB fallback in clear_cache! by replacing it with a scoped SCAN when a keyspace is configured. Moves session-token resolution and role-closure expansion from Parse::AtlasSearch::Session into Parse::Authorization::Context, one per Parse::Client, so two clients addressing two Parse apps no longer share identity or role caches. The Atlas Search session surface is kept as a deprecated shim delegating to the default client's context. Adds Parse::Cache::SubCache identity and role planes with per-subject generation counters and plane-wide epochs for sub-TTL invalidation. Parse::Cache::Invalidation registers after_save/after_delete/_Role, after_save/after_delete/_User, and after_logout/_Session webhook triggers that keep the planes honest across all clients without relying on application discipline. Adds Parse::Cache::UpstreamRoles, a read-only consumer of Parse Server's own role cache with strict validation and freshness gating. Parse::Cache::Redis gains verify_upstream_isolation! with a sentinel round-trip to distinguish an empty database from a genuinely separate one. Parse::MongoDB.verify_client! refuses a mongo-direct query authorized by a client whose application differs from the one the global connection was configured for, preventing a per-client authorization + process-global connection from silently reading the wrong application's data. All new behavior is opt-in via cache_keyspace: true; upgrading without that flag changes nothing.
Update the upstream-role isolation probe to keep only keys that match Parse Server's `<appId>:role:<userId>` format instead of rejecting keys by `parse-stack:` prefix. This avoids missing shared databases when an app is named `parse-stack` and avoids misclassifying this SDK's own role-plane keys on nondefault keyspace versions. Adds regression tests for both cases.
Add `client:` support to all public mongo-direct read entry points (`results_direct`, `count_direct`, `distinct_direct`, `distinct_direct_pointers`) and `Parse::MongoDB.aggregate`, then pass it into ACL scope resolution so client binding checks can compare the correct authorization context. This closes the gap where calls always resolved via `Parse.client`, making multi-client mismatch detection ineffective. Update README and CHANGELOG to document the new behavior, and add tests that verify `client:` reaches resolution, mismatched clients are rejected, and every direct entry point exposes the keyword.
Closes cross-application authorization gaps where an agent or query bound to a secondary client resolved tokens, walked role graphs, and read rows against the default application. Key changes: - `Agent#direct_auth_kwargs` adds the agent's client alongside its identity; `acl_scope_kwargs` stays clean for user-supplied splats - Sub-agents inherit their parent's client instead of defaulting to the process default - `MongoDB.collection` takes `authorizing_client:` replacing the unconditional default comparison - `MongoDB.verify_client!` fails closed for unidentified callers once two applications are seen; lazy binding is driven by the owner (default client), not call order - Atlas Search, vector search, and hybrid search forward the authorizing client to `run_atlas_pipeline!` / `run_pipeline!` - `ACLScope.client_of` extracts the client from a resolution safely for duck-typed stand-ins - Role graph traversal (`all_for_user`, `all_users`, `role_names_for_user`, `users_in_role_subtree`) carries `client:` through every recursive hop - `Parse::Cache::MonetaSurface` adds the derived Moneta surface (`[]=`, `fetch`, `values_at`, `slice`, `merge!`) to `Redis`, `ScopedView`, and `KeyspacedStore`; fixes infinite recursion from deriving `load` from `[]` - `client.sdk_cache` holds the keyspaced view; `client.cache` returns the configured store unchanged - `clear_cache!` operates on `sdk_cache` so application keys survive a scoped clear - `Parse.cache` is no longer memoized so it follows re-configuration - `SubCache#refresh_generation_expiry` drops the `store` fallback that caused lost updates on concurrent generation bumps - `Parse::Cache::Redis#expire` added for value-preserving TTL via PEXPIRE - `MONGO_AVAILABILITY_ERROR_NAMES` replaces the broken `defined?(Mongo::Error::ConnectionFailure)` guard in `Role`
Add a CHANGELOG entry describing new `can_read?`, `can_write?`, and `can_delete?` predicates on `Parse::User` and `Parse::Role`. The note explains how these checks combine ACL and CLP rules, include inherited role membership, preserve Parse public defaults when ACL is missing, and fail closed when role membership or CLP proof is unresolved.
Introduces `Parse::Access` with evidence-bearing `Decision` results (`allowed`, `denied`, `unknown`) and rewires `Parse::User` and `Parse::Role` access helpers to use it. The boolean `can_read?`/`can_write?`/`can_delete?` methods now fail closed unless access is definitively allowed, while new `access_decision` and `access_decisions` expose why a check succeeded, failed, or remained unknown. Hardens CLP and cache behavior by adding branch-aware `Parse::CLPScope.evaluate_access`, scoping CLP cache entries by Parse application client, and improving pointer-field and grouped user-field handling. It also tightens related infrastructure: accurate Redis delete counts, safer default TTLs for scoped identity and role planes, stricter upstream-role TTL freshness checks, and client-binding verification for hybrid vector search rank-fusion probes.
Updates the 5.7 release notes and caching docs to reflect the current cache API and behavior. The docs now consistently refer to `client.sdk_cache` and `Parse::Cache::ScopedView` for identity and role planes, clarify that keyspace coverage is for response, identity, and role keys, and tighten upstream role-cache guidance to describe compare-only default behavior and trust-boundary implications when `roles_for` is consumed directly.
Rollback state capture now snapshots Parse object property ivars instead of `attributes`, which only contains schema type metadata. The rollback path restores those property values directly and preserves ActiveModel dirty tracking internals without defining `@attributes`, preventing post-rollback mutation tracker errors. Added focused unit tests for value snapshotting, mutable value isolation, property restoration, dirty tracking behavior after rollback, and the `attributes` schema contract.
Cache Keyspacing, Client-Owned Authorization, and Access Decisions
A release focused on shared-Redis safety and fail-closed authorization: response, identity, and role-cache entries can move into a reserved app-scoped keyspace; clearing can no longer widen past what was asked; session and role resolution move off Atlas Search onto a client-owned
Parse::Authorization; and users and roles gain advisory ACL/CLP access decisions.Changes
Cache keys move into a reserved, app-scoped keyspace
Parse::Cache::Keyspaceowns the physical layout of response, identity, and role-cache keys, plus the glob patterns that clear them again.app_scopeis a digest of the application id and server URL, so two apps sharing one Redis no longer collide when neither sets acache_namespace:. Enable withcache_keyspace: trueonParse.setup; without it, the previous key shape and behavior are preserved exactly.Parse::Client#clear_cache!no longer reachesFLUSHDBwhen a keyspace is configured. Previously, the wrapper fell through to a full flush whenever no namespace was set (the default), destroying co-tenant data on a shared Redis and removingfirst_or_create!mutual exclusion by deleting the SDK's own create-locks. Withcache_keyspace: true, every clear is a scoped SCAN inside the client's own keys.Parse::Cache::ScopedViewexposed asclient.sdk_cacheacceptsfamily:andtenant:on#clear, and#delete_matching(pattern)evicts by glob within the view's own keyspace.UNLINKrather thanDELwhen available, and emits aparse.cache.evictnotification carryingpattern_digest,deleted, andduration_ms.The response cache's auth separation is now enforced by construction
Parse::Cache::Keyspace#cache_keyhas no default forauth:and raises rather than building a response-cache key without one, so the master-key/session-token/anonymous separation can no longer be lost by accident.Identity and role caching share one backend across processes
Parse::Cache::ScopedView#identityand#rolesreturnParse::Cache::SubCacheplanes forclient.authorization.configure; the legacy Atlas Search cache setters delegate to the same default-client context. Every worker can therefore resolve a session token or role closure against the same shared view instead of its own in-process cache.Role and session invalidation no longer depends on application discipline
Parse::Cache::Invalidationregisters webhook triggers (after_save/after_deleteon_Roleand_User,after_logouton_Session) that keep the identity and role planes honest, covering writes from any source Parse Server sees, not just this SDK. Disable withcache_invalidation_hooks: false.after_*triggers now accumulate handlers the wayafter_savealways has.Optional read of Parse Server's own role cache
Parse::Cache::UpstreamRolesreads the<appId>:role:<userId>closure Parse Server writes for itself viaclient.sdk_cache.upstream_roles.roles_for, attached by passingparse_cache_url:toParse::Cache::Redis. It is strictly read-only and observable-only in this release:compare_upstream_rolesemits aparse.cache.role_compareevent but does not change any ACL decision.Parse::Cache::Redis#verify_upstream_isolation!detects whether the SDK's cache database and Parse Server's role-cache database are actually the same Redis database, since a shared database means a_Rolewrite'sFLUSHDB(Parse Server 9.10.0 and earlier) destroys the SDK's cache and create-locks. See Saving a _Role clears the entire cache, issuing FLUSHDB on the Redis cache adapter parse-community/parse-server#10617.Session-token and role resolution move off Atlas Search and onto the client
Parse::Authorizationis the new owner of session-token resolution and role-closure expansion.client.authorizationreturns aParse::Authorization::Context, one perParse::Client, replacing module-level globals reachable only throughParse.client.Parse::Clientinstances addressing two different Parse applications no longer share one identity cache and one role cache. A session token minted by a secondary application could previously be validated against the default application's/users/meand its cached role closures.Parse::AtlasSearch.session_cache=,.role_cache=,.session_cache_ttl,.role_cache_ttl,.upstream_role_reader,.compare_upstream_roles, andParse::AtlasSearch::Session.resolve/.invalidate/.invalidate_user_roles/.reset_caches!still work and delegate to the default client's context. Slated for removal in 6.0.Mongo-direct reads stay bound to the authorizing client
Parse::MongoDB.verify_client!refuses a mongo-direct read authorized by a client for a different Parse application than the process-global MongoDB connection, preventing one application's identity and role claims from being applied to another application's rows.Parse::Query#results_direct,#count_direct,#distinct_direct,#distinct_direct_pointers, andParse::MongoDB.aggregateacceptclient:and carry it through ACL resolution and the final collection binding check.Users and roles can inspect effective object access
Parse::Access.check,Parse::Access::Decision, and thecan_read?/can_write?/can_delete?helpers combine object ACLs with class-level CLP, inherited roles,pointerFields,readUserFields, andwriteUserFields. Decisions areallowed,denied, orunknown; the boolean helpers accept only a definite allow and otherwise fail closed. These checks are advisory—the eventual Parse Server request remains authoritative.Cache clears can no longer widen past what was asked
Parse::Cache::Redis#clearacceptedfamily:/tenant:and silently ignored them, falling through toFLUSHDB. It now raisesArgumentErrorfor that combination.cache_keyspace: trueon a store that cannot produce a scoped view (a plainMoneta.new(:Redis)) left the store installed bare, soclear_cache!still issuedFLUSHDB. Such stores are now wrapped inParse::Cache::KeyspacedStore, which raisesParse::Cache::UnscopedClearRefusedrather than widening the clear.Other fixes
Parse::Cache::Redis#verify_upstream_isolation!reported an empty shared database as isolated. It now falls back to a sentinel write/read to establish sharing rather than trusting an empty SCAN result.Parse::Cache::SubCachenever expired, an unbounded Redis growth path on a public signup flow. They now expire at twice the plane's entry TTL.Parse::Role.all_for_user's existing default.Behavior Notes
cache_keyspace: trueis the opt-in switch for the new cache layout, scoped clearing, and invalidation hooks. Left unset, those cache behaviors remain exactly as before.parse_cache_url:must address a different Redis database fromurl:; the two have different clearing semantics and sharing one exposes the SDK's cache and create-locks to Parse Server's_RoleFLUSHDBbehavior.permission_stringsor an ACL decision. A caller that directly consumesroles_foras authorization input makes that database part of its trust base and should restrict the credential to+get +pttlon<appId>:role:*.See CHANGELOG.md for full detail on every change in this release.