Skip to content

v5.8.1 - Webhook ACLs, Query Authority, Session Revocation, Search Ownership

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 07 Oct 20:39
· 6 commits to main since this release
2eeba94

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_save handler that returns parse_object gets 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 _User class declared with owner: :self is 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 set ACL itself. A guard :acl, :master_only revert 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::Session or Parse::User objects, singly or through 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), 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 explicit master: true passed to Parse::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), and parse.mongodb.aggregate reports master_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_search raises inside the block, and a LiveQuery admin client that first connected inside it keeps withholding its master key on every reconnect. Signup and upgrade_anonymous! store timestamps as Parse::Date, so reading them no longer marks the user dirty.

Combined queries keep their authority and application

  • FIXED: clone, |, or_where, Parse::Query.or, :or branches (Hash or query), and $inQuery/$select subqueries keep the query's authority and application: session token, become client, scoped user or role, master-key flag, read preference, and the Parse application it targets. Different authorities or applications raise ArgumentError instead of running under one of them. Before, a session combined with scope_to_user ran as the scoped user, a become-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 become client 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, so reduce(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-separated order field 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 without preserve_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!, and clear_changes! discard them.
  • FIXED: RelationCollectionProxy#save sends the staged operations (it silently sent nothing) and accepts session:. 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 / readUserFields constraint 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.filter when 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: and methods: filters only narrow its parent's (they used to replace them), and a sub-agent cannot turn on master_atlas when 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/call no longer skips require_approval_for. Qualified methods: entries match class aliases (User / _User) and Ruby constant names, so an except: 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 :or branch 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_to for a field whose server column holds a plain String, declare it as a :string property instead.
  • Code that relied on master: true keeping master authority inside Parse.without_master_key should wrap that call in Parse.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 does master_atlas: true under a parent without it.
  • Parse Server sends a before_save webhook 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 returns true, 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 implementing invalidation_nonce and bump_invalidation_nonce.

Commit: 2eeba94
Author: Adrian Curtin
Date: October 7, 2026