Skip to content

v5.1.0 - File URL Hardening, Locks, Image Embeddings

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 03 Jun 20:21
· 36 commits to main since this release

Feature Release

Hardens Parse::File so signed URLs never persist in @url, promotes the Redis-backed lock to a public Parse::Lock primitive, scopes LiveQuery to ACL by default, adds URL-based image embeddings (embed_image) for Voyage and Cohere multimodal models, tenant-aware cache namespacing, and a new canonical ACL/CLP reference guide. Several Parse.setup / LiveQuery client-setup bugs are fixed along the way.

Breaking Changes

  • BREAKING: LiveQuery connections are now ACL-scoped by default. The connect frame no longer carries the master key merely because one is configured — Parse Server resolves master-key (ACL/CLP-bypass) authorization once per connection from the connect frame, so a Parse.setup(master_key: …) process previously elevated every session-token subscription the caller believed was ACL-scoped. To restore the admin/event-tap behavior, build an explicit admin connection with Parse::LiveQuery::Client.new(use_master_key: true) or Parse::LiveQuery.configure { |c| c.use_master_key = true }. Admin connections emit a one-time [Parse::LiveQuery:SECURITY] warning at connect.

Changes

Parse::File: presigned-URL stash and leak hardening

  • NEW: Parse::File#url= and #attributes= route every write through a single normalization point. When an incoming URL carries a recognized signed-URL parameter (X-Amz-Signature, X-Amz-Credential, X-Amz-Security-Token, AWSAccessKeyId, Key-Pair-Id), the query string is stripped, the bare canonical URL is stored in @url, and the original signed URL is stashed with a data-driven expiry parsed from the URL itself (SigV4 X-Amz-Date + X-Amz-Expires, or SigV2 / CloudFront Expires).
  • NEW: Parse::File#presigned_url, #presigned_url_expires_at, and #presigned_url_valid?(buffer: 60) expose the stashed signed URL and gate refetches. The stash is invalidated automatically on any URL reassignment, so callers are never handed a value staler than @url.
  • NEW: Parse::File.signed_url_policy (:strip default, or :raise for strict mode), Parse::File::SignedUrlError, and Parse::File.parse_presigned_expiry(url) for caller-side detection and enforcement.
  • NEW: Parse::File.log_filter, .log_filter_strict, and .filter_parameter_names produce ready-to-use regexes for lograge / semantic_logger / error-reporter scrubbers and Rails.application.config.filter_parameters, so an accidental Rails.logger.info(file_url) does not leak read capability into log aggregators.
  • FIXED: Parse::File#saved?, Hash#parse_file?, and Parse::File#save now strip the URL query string before basename comparison, so the short-TTL presigned URLs Parse Server's S3FilesAdapter returns on read and on create no longer cause false negatives or bake signature bytes into @name.
  • CHANGED: Parse::File#inspect no longer includes the URL. The new format emits @name, @mime_type, @contents (presence), and @url=set|blank — enough to debug, none of the URL content. #to_s is deliberately unchanged so ERB and <img src="<%= file %>"> callers keep working.

Parse::Lock: public TTL-bounded mutual-exclusion primitive

  • NEW: Parse::Lock.acquire(key, ttl:, wait:, on_degraded:) { |token| … } exposes the Redis-backed lock previously hidden inside first_or_create! as a first-class, block-form primitive. TTL-bounded (1..30s, default 3s), with an in-process Mutex fallback for process-local caches, fails closed, and hashes keys with SHA-256 before they reach the store.
  • NEW: Parse::Lock.acquire(secret:) HMAC keying picks up the operator-configured PARSE_STACK_LOCK_SECRET / Parse.synchronize_create_secret, so one secret hardens both Parse::Lock and Parse::CreateLock. Explicit secret: values are length-validated against Parse::Lock::SECRET_MIN_BYTES.
  • NEW: Parse::Lock::TimeoutError / UnavailableError (both inherit Parse::Error), and the shared Parse::LockBackend (@api private) now consumed by both Parse::Lock and Parse::CreateLock, eliminating the previous .send(:private) coupling. Parse::Lock is autoloaded.
  • FIXED: lock release is now an atomic compare-and-delete via a Lua script (Parse::Cache::Redis#lock_acquire / #lock_release), so a holder whose lease expired and was re-acquired by another holder can no longer delete the new holder's live lock.
  • FIXED: Parse::Lock.acquire no longer over-promises exactly-once execution — the contract is documented as mutual exclusion with a deadline, the block receives its owner token for fencing, and a warning is emitted on release when the critical section overran its TTL.

LiveQuery: ergonomics

  • NEW: Parse::LiveQuery is autoloaded; the autoload opens no socket (one only opens when LiveQuery is enabled and a client is instantiated).
  • NEW: Parse::LiveQuery::Client.new(use_master_key: true), the config.use_master_key toggle, and Client#admin_connection? make the ACL-bypassing posture explicit and inspectable.
  • NEW: Klass.subscribe / Query#subscribe / Client#subscribe accept a block yielded the Subscription before the subscribe frame is sent, so callbacks are wired before any server event can arrive. If the block raises, the subscription is rolled back out of the client registry before the exception propagates.
  • NEW: Parse::LiveQuery.run_until_signal!(client:, signals:, …) { |client| … } is a signal-safe shutdown helper for long-running subscribe sessions that avoids the ThreadError: can't be called from trap context raised by calling client.close directly from a Signal.trap block.
  • CHANGED: use_master_key: on a subscribe call is now an intent assertion, not a per-subscription wire credential — the subscribe frame never carries masterKey. On a non-admin connection it warns once and the subscription stays ACL-scoped.
  • FIXED: Client#inspect and Subscription#inspect redact @master_key, @client_key, and @session_token instead of dumping them in plaintext. SubscriptionError now carries structured request_id / class_name context.

Image embedding: embed_image DSL + Voyage / Cohere multimodal (URL-only)

  • NEW: embed_image source_field, into: :vector_property, … class macro mirrors embed for :file-typed sources. On before_save it extracts the file URL, validates it, and embeds — the digest is the SHA-256 of the URL (resaving the same URL is a no-op; pointing at a different URL re-embeds).
  • NEW: Parse::Embeddings::Voyage#embed_image (voyage-multimodal-3) and Parse::Embeddings::Cohere#embed_image (embed-v4.0) route image URLs to each provider's multimodal endpoint. URL-only — the SDK does not download image bytes in this release. Text-only models raise a clear BadRequestError before any network call.
  • NEW: Parse::Embeddings.validate_image_url! is the canonical SSRF-hardened URL validator — sentinel gate, https:// scheme, no userinfo, obfuscated-IP rejection, host allowlist (empty denies everything), port check, and CIDR resolution delegated to Parse::File.assert_host_allowed!.
  • NEW: Parse::Embeddings.trust_provider_url_fetch= (frozen-string sentinel "PROVIDER_EGRESS_VERIFIED") and Parse::Embeddings.allowed_image_hosts= gate URL forwarding to providers, mirroring the acl: :off sentinel pattern so accidental enablement via ENV interpolation is impossible.

MCP: structuredContent for 5 more tools

  • NEW: output_schema declarations on aggregate, export_data, atlas_text_search, atlas_autocomplete, and atlas_faceted_search so the dispatcher auto-mirrors their result Hash into structuredContent per MCP 2025-06-18, bringing built-in tool coverage to sixteen.

Caching: tenant-aware namespacing

  • NEW: Parse.with_cache_tenant(scope) { … } / Parse.current_cache_tenant compose a tenant into the cache key as <base-namespace>:T:<tenant>:…, giving a multi-tenant app sharing one Redis per-tenant key isolation and per-tenant SCAN-eviction without per-tenant client plumbing. Fiber-local, restored on block exit even when the block raises; carries :cache_tenant on the parse.cache.* ActiveSupport::Notifications payload.

Parse::Installation / Parse::User associations and field visibility

  • NEW: Parse::Installation belongs_to :user and Parse::User has_many :installations expose the installation user pointer for targeted push grouping (master-key only on the has_many side, since Parse Server hardcodes _Installation find to master-key).
  • NEW: Parse::User.master_only_fields(*fields) and Parse::User.self_visible_fields(*fields, via: :self) declare field visibility on _User, with one-time advisories documenting the protectedFieldsOwnerExempt: false server-option and self-pointer prerequisites.
  • NEW: One-time CLP advisory on Parse::Installation enumerating which operations CLP actually controls, since Parse Server hardcodes find / delete to master-key and gates create / update on the installation-id header rather than CLP.

Client setup fixes

  • FIXED: Parse.setup silently no-op'd on every call after the first (it routed through Parse::Client.new's ||= default-client guard). It now delegates to Parse::Client.setup, so re-configuring the default client works without manually clearing the :default slot.
  • FIXED: Passing live_query_url: / live_query: { … } to Parse.setup no longer raises ArgumentError — the configuration is now applied through the block form. Top-level live_query_url: correctly wins over live_query: { url: … }, an explicit ws:// URL against a non-loopback host is refused unless allow_insecure: true, and unknown live_query: keys now warn instead of being silently dropped.

Documentation

  • NEW: docs/acl_clp_guide.md is the canonical reference for ACL, CLP, protectedFields, role hierarchy, and field-guard write protection — covering the five enforcement layers, the system-class CLP matrix, the _Installation hardcoded asymmetry, the _User field-visibility recipe, and the REST-aggregate vs mongo-direct enforcement asymmetry. The client SDK guide and YARD @notes on the master-key-only system classes were updated to match.

Commit: 456373c
Author: Adrian Curtin
Date: June 3, 2026