Repository navigation
v5.8.1 - Webhook ACLs, Query Authority, Session Revocation, Search Ownership
Correctness Release
A correctness release that closes the follow-ups left from 5.8.0: webhook create ACLs and sub-document writes, session revocation and Parse.without_master_key parity, query authority across OR composition, subqueries, LiveQuery, and push, relation change tracking, search ownership filtering, and sub-agent policy narrowing. There are no new defaults and no API changes; outcomes that change are listed under Behavior Notes, and CHANGELOG.md has the full detail.
Changes
Webhook creates get the ACL their class policy resolves
- FIXED: A client create through a
before_savehandler that returnsparse_objectgets the ACL its class policy resolves, exactly as an SDK-side 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 or master-key request gets the policy fallback, and a_Userclass declared withowner: :selfis owned by the new user, never the requester. - FIXED: An ACL the handler assigns (through
acl=or mass assignment) always wins, even when it equals the class default, whatever the handler returns:parse_object,true,nil, or a Hash that does not setACLitself. Aguard :acl, :master_onlyrevert on a create returns the ACL to the class policy instead of{}. - FIXED: When a handler changes a record, a sub-document the client changed is written back as one-level dotted paths instead of the whole field, so concurrent writes to other sub-keys survive. Dotted keys in a returned Hash fold into the write they belong to with operators (
Increment,Add,AddUnique,Remove,Delete) applied rather than stored, and unsupported or conflicting overrides fail the save with an error.
Revoked sessions stop resolving, and without_master_key means it
- FIXED: Deleting
Parse::SessionorParse::Userobjects, singly or throughArray#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), and a session already gone uses the owner recorded when it was last fetched. Only a failed lookup resets the identity cache, at most once every 5 seconds per client, so an endpoint that deletes caller-supplied session ids cannot flush a shared Redis plane. - FIXED: A token resolution in flight when an invalidation lands no longer caches its answer, in this process or, on the Redis identity plane, in any other. The resolver checks a shared invalidation marker before and after writing and evicts its own write when the marker moved, including across another process's full reset.
- FIXED: Inside
Parse.without_master_key, an explicitmaster: truepassed toParse::MongoDB.aggregate,Parse::ACLScope, direct query reads, vector and hybrid search, Atlas Search, or agent tools for master-key agents runs in the public scope (or raises under strict mode), andparse.mongodb.aggregatereportsmaster_dropped: true. SDK metadata reads (class schema, role graph, index statistics, Atlas index listing) keep the master key through a private marker that never leaves the outgoing request. Before, a schema read refused inside the block was cached and denied every scoped mongo-direct read of that class, for all callers, for 5 seconds. - FIXED:
faceted_searchraises inside the block, and a LiveQuery admin client that first connected inside it keeps withholding its master key on every reconnect. Signup andupgrade_anonymous!store timestamps asParse::Date, so reading them no longer marks the user dirty.
Combined queries keep their authority and application
- FIXED:
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-key flag, read preference, and the Parse application it targets. Different authorities or applications raiseArgumentErrorinstead of running under one of them. Before, a session combined withscope_to_userran as the scoped user, abecome-client query composed into an OR ran as the master key, and a clone could send one application's session token to the default application. - FIXED: Constraints that only the aggregation pipeline or mongo-direct can run (
array_size,set_equals,readable_by, mongo-direct geo operators) raise inside an OR, a subquery, a LiveQuery subscription, or push targeting. Before, they were dropped and the query, subscription, or push reached every row or installation. - FIXED: LiveQuery subscriptions run under the query's own session or
becomeclient and refuse to send it to a LiveQuery server for a different Parse application. - FIXED: An OR with an unconstrained branch matches every row (an empty receiver of
|still starts the OR, soreduce(Model.query, :|)works), and:or => [...]builds a real$or. A bare objectId compared to a declared pointer field matches on REST, LiveQuery, and push. A repeated or comma-separatedorderfield is sent once, and"-field"parses as descending.
Staged relation changes survive fetches and saves correctly
- FIXED: A fetch (
fetch!, partial fetch, autofetch, with or withoutpreserve_changes:) keeps a relation's staged additions and removals and leaves the relation marked changed, including when the response carries the Relation descriptor.rollback!,reload!, andclear_changes!discard them. - FIXED:
RelationCollectionProxy#savesends the staged operations (it silently sent nothing) and acceptssession:. Atomic relation operations write to the relation's Parse column when it differs from the Ruby name, and pointer collections adopt the array Parse Server returns only when every entry is a pointer to the declared class.
Search ownership is applied before the page is cut
- FIXED: Atlas Search, autocomplete, and native hybrid search apply the CLP
pointerFields/readUserFieldsconstraint before$limit, so a scoped caller no longer gets a short or empty page when other users' rows rank first. - IMPROVED: Vector search enforces ownership server-side and pushes it into
$vectorSearch.filterwhen every owner field is a declared scalar pointer filter-indexed in a READY index serving its latest definition. A refusal naming an owner path is retried once without the prefilter, and a caller's own unindexed filter no longer disables it for everyone.
Sub-agents cannot widen their parent's policy
- FIXED: A sub-agent's
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; before, its faceted search returned every row under the parent's scoped identity. - FIXED: Sub-agents use the parent's approval gate at call time, including through the MCP dispatcher, so a sub-agent built inside a
tools/callno longer skipsrequire_approval_for. Qualifiedmethods:entries match class aliases (User/_User) and Ruby constant names, so anexcept:entry cannot be bypassed by another spelling.
Behavior Notes
- Combining queries that run under different authorities or against different Parse applications raises
ArgumentError, as does changing the authority or client of a combined query. Query-shaping options (limit,order,keys) inside an:orbranch raise. - LiveQuery subscriptions, push targeting, and subqueries with pipeline-only constraints raise. An OR with an unconstrained branch returns every row.
- REST, LiveQuery, and push comparisons of a bare objectId to a declared pointer field now return the matching rows. If a model declares
belongs_tofor a field whose server column holds a plain String, declare it as a:stringproperty instead. - Code that relied on
master: truekeeping master authority insideParse.without_master_keyshould wrap that call inParse.with_master_key. The block guards against accidental master-key use and is not an isolation boundary. - A sub-agent
only:list that leaves nothing once the parent's filters apply raises, as doesmaster_atlas: trueunder a parent without it. - Parse Server sends a
before_savewebhook only the resulting value of a dotted operator, so two concurrent writes inside the same sub-key can still overwrite each other, and a handler that returnstrue,nil, or a Hash without assigning an ACL keeps the client's write as sent. - A session deleted outside the SDK whose owner this process never recorded keeps resolving until the next rate-limited reset or
identity_cache_ttl. A custom identity plane gets the cross-process revocation guarantee by implementinginvalidation_nonceandbump_invalidation_nonce.
Commit: 2eeba94
Author: Adrian Curtin
Date: October 7, 2026