Repository navigation
v5.8.1 - Webhook ACLs, Query Authority, Session Revocation, Search Ownership - #62
Merged
Merged
Conversation
Inside Parse.without_master_key an explicit master: true to Parse::ACLScope, Parse::MongoDB.aggregate, vector and hybrid search, Atlas Search, or agent tools for master-key agents now resolves to the public scope (or raises ACLRequired under either strict flag), and the parse.mongodb.aggregate notification reports master_dropped: true. SDK metadata reads keep the master key through a private sentinel instead of toggling fiber state: the CLP schema fetch, the role-graph walk, index statistics, and the Atlas Search index listing. A schema read refused inside the block is no longer negative-cached, which had denied every scoped mongo-direct read of the class for all callers for 5s. faceted_search raises inside the block, and a LiveQuery admin connection opened there sends no master key. Deleting sessions or users, singly or through Array#destroy, now drops their identity-cache entries on every attempted delete. A session missing its token or owner is looked up first (one query per batch); if that fails the client's identity cache is reset. The looked-up token is cleared from the object on every path. Signup and upgrade_anonymous! store timestamps as Parse::Date so reading them no longer dirties the user.
Atlas Search, autocomplete, and native hybrid search apply the CLP pointerFields / readUserFields constraint as a $match before $limit (and inside each hybrid input), so a scoped caller no longer gets a short or empty page when other users' rows rank first. Vector search enforces ownership server-side across the candidate window and pushes it into $vectorSearch.filter when every owner field is a declared scalar pointer filter-indexed in a READY index serving its latest definition. A refused prefilter is retried once without it and skipped for the cache TTL, and a failed index lookup is negatively cached.
An OR with an unconstrained branch matches every row (an empty
receiver of | still starts the OR), :or => [...] builds a real $or,
and pipeline-only constraints raise inside any OR instead of compiling
to {}. A repeated order field is sent once and "-field" strings parse
as descending. A bare objectId on a declared pointer field is sent as
a Pointer on REST, LiveQuery subscriptions, and push targeting via the
new Query#compile_rest_where.
clone, |, or_where, Parse::Query.or, and :or query branches keep the
query's authority (session token, become client, scoped user or role,
master flag, read preference). Different authorities raise, a rejected
change leaves the query untouched, and a combined query re-checks its
authority before it runs. Docs use pointer_id / array_size.
…safely A fetch (including partial fetch, autofetch, and a response carrying a Relation descriptor) keeps a relation's staged additions and removals and leaves the relation marked changed. clear_changes! and reload! discard them. RelationCollectionProxy#save sends the staged ops, and atomic relation ops target the remote column. Pointer collections adopt the array Parse Server returns only when every entry is a pointer to the declared class, falling back to local application.
…y path A client create through a before_save handler gets the ACL its class policy resolves, taking the requesting user as owner for owner policies, so a signed-in client can read its own record and nothing is left public by omission. An ACL the handler assigns always wins, whatever the handler returns. When the handler changes a record, changed sub-documents are written back as dotted paths at any depth, falling back to a whole write for arrays and typed values, and dotted overrides fold into whole writes to avoid path conflicts. Webhook tests now save and restore global webhook state.
A sub-agent's tools: filter intersects its parent's and methods: filters are kept as layers, so a child can no longer enable a tool or agent method its parent was not given. Sub-agents use the parent's approval gate at call time, qualified methods: entries match class aliases (User and _User), and describe and the audit payload report inherited method layers.
Bump the version and add the 5.8.1 CHANGELOG entry. Correct the lease limits in the lock backend comment.
merge_or_scope! pinned the combined authority only when it was adopted from the incoming query, so alice_query | unscoped left Alice's session unpinned and a later session_token = nil let the query run as master. The receiver's own authority is now pinned when the other side has the default authority, for |, or_where, Parse::Query.or, and :or branches.
A _User class declared with owner: :self is owned by the new user, never the requester, and its create no longer pre-generates an objectId the server never sees. A master-key request does not adopt its user as owner. A guard :acl revert on a create returns the ACL to the class policy, and an ACL set through mass assignment counts as handler assigned. Changed sub-documents are written back one level deep, since Parse Server rebuilds dotted keys only one level for afterSave. Dotted override operators are applied when folded into a whole write, and unsupported or conflicting overrides fail the save. Hash override keys may use Ruby names, and the webhook test state helper copies routes.
A session lookup that finds no row leaves nothing to forget; only a lookup that fails resets the identity cache, at most once every 5s per client. A client without the master key looks sessions up with the delete's own session. Caches are cleared only when the delete succeeds or the row is already gone, the lookup bypasses the response cache, and the destroy hooks are private. A token resolution in flight across an invalidation no longer caches its answer. The metadata marker rides only on the outgoing request copy, failed schema reads inside without_master_key are retried at most every 2s, and the role walk skips the response cache.
Queries combined with OR, through :or Hash or query branches, or as $inQuery/$select subqueries must target the same Parse application and run under the same authority; clone keeps any explicit client. A rejected composition leaves the receiver unchanged, and explicit auth passed to direct or Atlas terminals must match a pinned authority. LiveQuery subscriptions and push targeting refuse pipeline-only constraints instead of widening to every row, and subscriptions run under the query's own session or become client. A LiveQuery admin client that first connected inside without_master_key keeps withholding its master key on reconnect. order splits comma strings.
rollback! drops staged relation changes, including after a fetch. A relation proxy kept across a fetch is unloaded so the next read re-queries the server. Relation saves accept session: and wait for a client-assigned objectId's create to return. Proxy field lists are cached per class.
A sub-agent can no longer enable master_atlas under a parent without it. The MCP dispatcher snapshots an agent's own approval gate so a sub-agent keeps delegating to its parent. Qualified methods: entries may name a model by Ruby constant, unresolved entries warn, and the audit payload reports a single inherited methods layer. Vector search treats only an owner-path refusal as a prefilter refusal, and native hybrid uses the default index when none is named.
Every invalidation moves a marker before dropping entries, and a token resolution checks it before and after writing, evicting its own write when it moved. On a shared plane the marker is a random nonce created before each lookup, so a reset in another process (which clears the marker before installing a new one) cannot leave a revoked identity cached. Session objectId to owner records, kept in their own bounded store and recorded by the client that fetched the session, let a delete of an already-gone session invalidate its owner; without one, a batch delete that applied or found the row gone falls back to the rate-limited reset, and a denied delete never resets.
A subscription that carries the query's session (an explicit token or a become client) checks that the LiveQuery client targets the same Parse application id as the query and raises otherwise, through both client.subscribe(query) and query.subscribe(client:). _Session query decoding records session owners against the fetching client. Initialize locals that CodeQL flagged as possibly uninitialized.
A custom identity plane gets the cross-process revocation-race guarantee by implementing invalidation_nonce and bump_invalidation_nonce. One with generation counters only is covered for single-token and per-user invalidations, but a lookup racing another process's full reset is bounded by identity_cache_ttl when its clear also deletes the counters.
… test with_or_rollback and the webhook test helper now capture their saved state before an explicit begin, so the rescue/ensure never reads a variable the capture could have left unset. query_compile_fixes_test sets up the default client before each test: Parse::Client.new registers itself as the default when none exists, so under some seeds an app B client became the default and broke the cross-application tests (seen on CI macOS Ruby 3.2).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Release 5.8.1: a correctness release that closes the follow-ups left from 5.8.0. It covers webhook create ACLs and sub-document writes, session revocation and
Parse.without_master_keyparity, query OR compilation and authority, relation change tracking, search ownership filtering, and sub-agent policy narrowing. There are no new defaults and no API changes; every corrected outcome is listed in the CHANGELOG under Behavior Notes.Webhooks
before_savehandler that returnsparse_objectgets the ACL its class policy resolves, as an SDK create would. Owner policies take the requesting user as owner when the owner field is empty, so a signed-in client can read its own record (it used to get{}under:owner_else_private). An anonymous create gets the declared fallback, never public write._Userclass withowner: :selfis owned by the new user, never the requester. Aguard :acl, :master_onlyrevert returns the ACL to the class policy.acl=or mass assignment) always wins, even when it equals the class default, and whatever the handler returns (parse_object,true,nil, or a Hash withoutACL).Sessions and master-key scope
Array#destroy, drops their cached identities when the delete succeeds or the row is already gone; a denied delete leaves the cache alone. A session missing its token or owner is looked up first (one query per batch, with the delete's own session when the client has no master key). A lookup with no row forgets nothing; only a failed lookup resets the identity cache, at most once every 5 seconds per client. The looked-up token never stays on the object or enters the response cache, and a token resolution in flight across an invalidation does not cache its answer.Parse.without_master_key, an explicitmaster: truetoParse::MongoDB.aggregate,Parse::ACLScope, direct query reads, vector and hybrid search, Atlas Search, and master-key agent tools runs in the public scope (or raises under either strict flag), and the aggregate notification reportsmaster_dropped: true.faceted_searchraises inside the block. The metadata marker rides only on the outgoing request copy, never onresponse.request. A LiveQuery admin client that first connected inside the block keeps withholding its master key on reconnect untilallow_master_key_connection!.upgrade_anonymous!store timestamps asParse::Date, so reading them no longer dirties the user.Queries, LiveQuery, and push
|still starts the OR, soreduce(Model.query, :|)works), and:or => [...]builds a real$or.readable_by,array_size,set_equals, mongo-direct geo operators) raise inside any OR, in subqueries, in LiveQuery subscriptions, and in push targeting, instead of being dropped and widening the result to every row.clone,|,or_where,Parse::Query.or,:orbranches (Hash or query), and$inQuery/$selectsubqueries keep the query's authority and application: session token,becomeclient, scoped user or role, master flag, read preference, and the target app. Different authorities or applications raise, the authority is pinned whichever operand carries it, a rejected change leaves the query untouched, and a combined query re-checks its authority (including explicit auth passed to direct or Atlas reads) before it runs. Subscriptions run under the query's own session.Query#compile_rest_where).orderfield is sent once, and"-field"strings parse as descending.Associations
fetch!, partial fetch, autofetch, with or withoutpreserve_changes:) keep a relation's staged additions and removals and leave it marked changed, including when the response carries the Relation descriptor.reload!andclear_changes!discard them.rollback!drops staged relation changes, including after a fetch, and a relation kept across a fetch re-reads its list.RelationCollectionProxy#savesends the staged operations (it silently sent nothing) and acceptssession:, and atomic relation operations target the remote column.Search
pointerFields/readUserFieldsownership constraint before$limit, so scoped callers no longer get short or empty pages when other users' rows rank first.$vectorSearch.filterwhen every owner field is a declared scalar pointer that is filter-indexed in a READY index serving its latest definition. A refusal naming an owner path is retried once without the prefilter, a caller's own unindexed filter no longer disables it for everyone, and failed index lookups are negatively cached.Agent policies
tools:andmethods:filters only narrow its parent's (they used to replace them), and a sub-agent cannot turn onmaster_atlaswhen its parent does not have it. Sub-agents use the parent's approval gate at call time, including through the MCP dispatcher, qualifiedmethods:entries match class aliases and Ruby constant names, and describe and the audit payload report inherited method layers.Behavior changes for upgrading apps
CHANGELOG.md lists each one. The ones most likely to be noticed:
ArgumentError, as does changing a combined query's authority.master: trueinsideParse.without_master_keyno longer runs as master; wrap such calls inParse.with_master_key.only:list that leaves nothing once the parent's filters apply raises, as doesmaster_atlas: trueunder a parent without it.before_savecreate replies carry the class-policy ACL.Testing
bundle exec rake test:unitpasses on the final commit.