Skip to content

v5.7.0 - Keyspaced Cache, Client-Owned Authorization, Access Decisions - #32

Merged
AdrianCurtin merged 12 commits into
mainfrom
dev_v570
Aug 1, 2026
Merged

v5.7.0 - Keyspaced Cache, Client-Owned Authorization, Access Decisions#32
AdrianCurtin merged 12 commits into
mainfrom
dev_v570

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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

  • NEW: Parse::Cache::Keyspace owns the physical layout of response, identity, and role-cache keys, plus the glob patterns that clear them again. app_scope is a digest of the application id and server URL, so two apps sharing one Redis no longer collide when neither sets a cache_namespace:. Enable with cache_keyspace: true on Parse.setup; without it, the previous key shape and behavior are preserved exactly.
  • FIXED: Parse::Client#clear_cache! no longer reaches FLUSHDB when 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 removing first_or_create! mutual exclusion by deleting the SDK's own create-locks. With cache_keyspace: true, every clear is a scoped SCAN inside the client's own keys.
  • NEW: The keyspace-bound Parse::Cache::ScopedView exposed as client.sdk_cache accepts family: and tenant: on #clear, and #delete_matching(pattern) evicts by glob within the view's own keyspace.
  • CHANGED: Scoped eviction issues UNLINK rather than DEL when available, and emits a parse.cache.evict notification carrying pattern_digest, deleted, and duration_ms.

The response cache's auth separation is now enforced by construction

  • IMPROVED: Parse::Cache::Keyspace#cache_key has no default for auth: 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.
  • FIXED: A non-GET write now invalidates the resource for every caller. Previously, invalidation could only name the anonymous variant, the master-key variant, and the caller's own, leaving every other user reading a stale copy until TTL expiry. Every auth variant now shares a key prefix so a scan-capable store evicts all of them together.
  • CHANGED: Invalidation also deletes the pre-keyspace form of a key, so a rolling deploy doesn't leave old workers serving entries a new worker's write should have killed.

Identity and role caching share one backend across processes

  • NEW: Parse::Cache::ScopedView#identity and #roles return Parse::Cache::SubCache planes for client.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.
  • NEW: Each plane carries a per-subject generation counter and a plane-wide epoch for invalidating entries that cannot be named directly (identity entries are keyed by session token, with no reverse map from user id).

Role and session invalidation no longer depends on application discipline

  • NEW: Parse::Cache::Invalidation registers webhook triggers (after_save/after_delete on _Role and _User, after_logout on _Session) that keep the identity and role planes honest, covering writes from any source Parse Server sees, not just this SDK. Disable with cache_invalidation_hooks: false.
  • FIXED: Registering a webhook handler previously replaced any handler already registered for the same non-rejectable trigger instead of composing with it. Non-rejectable after_* triggers now accumulate handlers the way after_save always has.

Optional read of Parse Server's own role cache

  • NEW: Parse::Cache::UpstreamRoles reads the <appId>:role:<userId> closure Parse Server writes for itself via client.sdk_cache.upstream_roles.roles_for, attached by passing parse_cache_url: to Parse::Cache::Redis. It is strictly read-only and observable-only in this release: compare_upstream_roles emits a parse.cache.role_compare event but does not change any ACL decision.
  • NEW: 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 _Role write's FLUSHDB (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

  • NEW: Parse::Authorization is the new owner of session-token resolution and role-closure expansion. client.authorization returns a Parse::Authorization::Context, one per Parse::Client, replacing module-level globals reachable only through Parse.client.
  • FIXED: Two Parse::Client instances 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/me and its cached role closures.
  • DEPRECATED: Parse::AtlasSearch.session_cache=, .role_cache=, .session_cache_ttl, .role_cache_ttl, .upstream_role_reader, .compare_upstream_roles, and Parse::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

  • FIXED: 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.
  • NEW: Parse::Query#results_direct, #count_direct, #distinct_direct, #distinct_direct_pointers, and Parse::MongoDB.aggregate accept client: and carry it through ACL resolution and the final collection binding check.

Users and roles can inspect effective object access

  • NEW: Parse::Access.check, Parse::Access::Decision, and the can_read?/can_write?/can_delete? helpers combine object ACLs with class-level CLP, inherited roles, pointerFields, readUserFields, and writeUserFields. Decisions are allowed, denied, or unknown; 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

  • FIXED: Parse::Cache::Redis#clear accepted family:/tenant: and silently ignored them, falling through to FLUSHDB. It now raises ArgumentError for that combination.
  • FIXED: cache_keyspace: true on a store that cannot produce a scoped view (a plain Moneta.new(:Redis)) left the store installed bare, so clear_cache! still issued FLUSHDB. Such stores are now wrapped in Parse::Cache::KeyspacedStore, which raises Parse::Cache::UnscopedClearRefused rather than widening the clear.

Other fixes

  • FIXED: 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.
  • FIXED: Generation keys in Parse::Cache::SubCache never expired, an unbounded Redis growth path on a public signup flow. They now expire at twice the plane's entry TTL.
  • CHANGED: Raised the MongoDB role-graph query default and hard cap from 6 to 10, matching Parse::Role.all_for_user's existing default.
  • CHANGED: The integration test stack pins Parse Server 9.10.0 (up from 9.9.0) and backs Parse Server's own session/user/role caches with Redis so upstream role entries are observable from outside the container.

Behavior Notes

  • cache_keyspace: true is 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 from url:; the two have different clearing semantics and sharing one exposes the SDK's cache and create-locks to Parse Server's _Role FLUSHDB behavior.
  • The built-in upstream-role integration is compare-only and never changes permission_strings or an ACL decision. A caller that directly consumes roles_for as authorization input makes that database part of its trust base and should restrict the credential to +get +pttl on <appId>:role:*.
  • Access decisions are fail-closed policy preflights, not substitutes for the authoritative Parse Server request.

See CHANGELOG.md for full detail on every change in this release.

Copilot AI review requested due to automatic review settings August 1, 2026 09:41
@AdrianCurtin AdrianCurtin changed the title Dev v570 v5.7.0 - Keyspaced Cache, Auth Separation, Client-Owned Authorization Aug 1, 2026
Comment thread lib/parse/cache/redis.rb Fixed
@AdrianCurtin
AdrianCurtin force-pushed the dev_v570 branch 2 times, most recently from 9cccbf3 to 0ed8a35 Compare August 1, 2026 09:43

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.

🟡 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 per Parse::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, but store is a writer method, not an accessor. The remedy should reference wrapped (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.

Comment thread lib/parse/cache/upstream_roles.rb
Comment thread lib/parse/cache/keyspace.rb
Comment thread lib/parse/cache/scoped_view.rb
Copilot AI review requested due to automatic review settings August 1, 2026 09:44

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.

🟡 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::UpstreamRoles uses JSON.parse and Set but does not require json or set, so requiring this file directly can raise NameError (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, but store is the Moneta writer method on this wrapper, not an accessor to the wrapped backend. This guidance is misleading; it should point at wrapped (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 UnscopedClearRefused message tells operators to call client.cache.store.clear, but store is the write method and won’t give access to the underlying store. This should reference client.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.

Comment thread lib/parse/cache/redis.rb
Copilot AI review requested due to automatic review settings August 1, 2026 09:48

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.

🟡 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! reports deleted as keys.size per 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 in parse.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 = nil is mis-indented relative to the surrounding assignments in reset!, 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.

Copilot AI review requested due to automatic review settings August 1, 2026 09:53

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.

🟡 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 / case block is inconsistent (the case and its comments are aligned with guard 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_logout branch and the else branch should be indented under the case statement, and the closing ends 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 do block 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 under guard 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 = nil is 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.

Copilot AI review requested due to automatic review settings August 1, 2026 10:03

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.

🟡 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

  • roles defaults ttl: to nil, so a caller that omits ttl: 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

  • identity defaults ttl: to nil, which creates a plane with no default TTL. If this is the first call (e.g. via webhook invalidation before the app configures Parse::Authorization), SubCache#generation_ttl becomes 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 and delete_matching eviction to Parse::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 and delete_matching is 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.

Copilot AI review requested due to automatic review settings August 1, 2026 10:15

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.

🟡 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.

Comment thread lib/parse/cache/scoped_view.rb Outdated
Copilot AI review requested due to automatic review settings August 1, 2026 12:34

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.

🟡 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 = nil is misindented relative to the other assignments in reset!, 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.

Comment thread lib/parse/cache/sub_cache.rb
Copilot AI review requested due to automatic review settings August 1, 2026 14:01

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Comment thread lib/parse/access.rb Fixed
Copilot AI review requested due to automatic review settings August 1, 2026 17:48

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 1, 2026 17:50

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings August 1, 2026 18:21

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@AdrianCurtin AdrianCurtin changed the title v5.7.0 - Keyspaced Cache, Auth Separation, Client-Owned Authorization v5.7.0 - Keyspaced Cache, Client-Owned Authorization, Access Decisions Aug 1, 2026
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.
Copilot AI review requested due to automatic review settings August 1, 2026 19:57

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

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.
Copilot AI review requested due to automatic review settings August 1, 2026 20:07

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@AdrianCurtin
AdrianCurtin merged commit 0991842 into main Aug 1, 2026
11 checks passed
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.

3 participants