parse-stack-next 5.6.0 → 5.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +489 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +219 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +366 -113
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +6 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +6 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +136 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +120 -5
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
data/README.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
A full-featured Ruby client SDK for [Parse Server](http://parseplatform.org/). [parse-stack-next](https://github.com/neurosynq/parse-stack-next) is a Ruby client SDK, REST client, and Active Model ORM for [Parse Server](http://parseplatform.org/), combining a low-level API client, a query engine, an object-relational mapper (ORM), and a Cloud Code Webhooks rack application in a single gem.
|
|
6
6
|
|
|
7
|
+
## What's new in 5.7
|
|
8
|
+
|
|
9
|
+
- **5.7.0: Reserved, app-scoped cache keyspace.** `Parse::Cache::Keyspace` lays out response, identity, and role-cache keys on a shared backend (`parse-stack:v1:<app_scope>[:<namespace>]:<family>[:T:<tenant>]:<rest>`) and owns the glob patterns that clear them again, so key generation and eviction can no longer drift apart. `app_scope` is a digest of the application id and server URL, so two apps sharing one Redis no longer collide. Enable with `cache_keyspace: true` on `Parse.setup`; left unset, behavior is unchanged. See [CHANGELOG.md](./CHANGELOG.md)
|
|
10
|
+
- **5.7.0: `clear_cache!` stops falling back to `FLUSHDB`.** With `cache_keyspace: true`, `Parse::Client#clear_cache!` performs a scoped SCAN inside the client's own keys instead of flushing the whole database, which previously could destroy co-tenant data and drop `first_or_create!` create-locks on a shared Redis. `flush_db!` remains the explicit opt-in for a full flush. See [CHANGELOG.md](./CHANGELOG.md)
|
|
11
|
+
- **5.7.0: Response-cache auth separation enforced by construction.** `Parse::Cache::Keyspace#cache_key` now requires an `auth:` discriminator for the response-cache family and refuses to build a key without one, so a master-key body and a session-token body can no longer land under the same key by accident. A non-GET write now invalidates every auth variant of a resource in one scoped pattern instead of only the variants the process has already seen. See [CHANGELOG.md](./CHANGELOG.md)
|
|
12
|
+
- **5.7.0: Shared identity and role planes.** `Parse::Cache::ScopedView#identity` and `#roles`, reached through `client.sdk_cache`, return `Parse::Cache::SubCache` planes for the client's authorization context, so every worker resolves a session token or role closure against one shared backend instead of its own in-process cache. Each plane invalidates by per-subject generation counter and plane-wide epoch rather than needing to enumerate entries it cannot name. See [CHANGELOG.md](./CHANGELOG.md)
|
|
13
|
+
- **5.7.0: Authorization becomes a client-owned module, not an Atlas Search internal.** `Parse::Authorization` now owns session-token resolution and role-closure expansion. `client.authorization` returns a `Parse::Authorization::Context`, one per `Parse::Client`, so two clients addressing two Parse applications no longer share one identity cache and one role cache, and a mongo-direct query with no `$search` anywhere in it no longer resolves identity through the Atlas Search namespace. `Parse::AtlasSearch.session_cache=`, `.role_cache=`, and `Parse::AtlasSearch::Session` remain as deprecated aliases for the default client's context, slated for removal in 6.0. See [CHANGELOG.md](./CHANGELOG.md)
|
|
14
|
+
- **5.7.0: Mongo-direct reads stay bound to the authorizing client.** `Parse::MongoDB.verify_client!` rejects a direct read when its authorization client belongs to a different Parse application than the process-global MongoDB connection. Direct-query entry points accept `client:` and carry it through authorization and collection binding. See [CHANGELOG.md](./CHANGELOG.md)
|
|
15
|
+
- **5.7.0: Effective access decisions for users and roles.** `Parse::Access.check`, `Parse::Access::Decision`, and the `can_read?` / `can_write?` / `can_delete?` helpers combine object ACLs with class-level permissions and inherited roles. Unknown evidence fails closed, and the eventual Parse Server request remains authoritative. See [CHANGELOG.md](./CHANGELOG.md)
|
|
16
|
+
- **5.7.0: Cache invalidation no longer depends on application discipline.** `Parse::Cache::Invalidation` registers webhook triggers on `_Role`, `_User`, and `_Session` (`after_save`/`after_delete`/`after_logout`) that keep the identity and role planes honest for writes from any client, not only the app's own logout and role-mutation code paths. Installs alongside the keyspace; disable with `cache_invalidation_hooks: false`. See [CHANGELOG.md](./CHANGELOG.md)
|
|
17
|
+
- **5.7.0: Optional read of Parse Server's own role cache.** `Parse::Cache::UpstreamRoles` can read the `<appId>:role:<userId>` closure Parse Server already wrote for itself. Role resolution does not consume it: the SDK still computes its own closure, and the only built-in integration is `compare_upstream_roles`, which emits a `parse.cache.role_compare` event so the two can be reconciled before anything depends on the upstream value. Call `roles_for` directly to use it. Strictly read-only, degrades to a miss on any anomaly, and `Parse::Cache::Redis#verify_upstream_isolation!` reports whether the two Redis endpoints share one database. See [CHANGELOG.md](./CHANGELOG.md)
|
|
18
|
+
|
|
19
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full 5.7 entry, including behavior notes and a complete setup example.
|
|
20
|
+
|
|
21
|
+
## What's new in 5.6
|
|
22
|
+
|
|
23
|
+
- **5.6.0: Voyage embeddings reach the Atlas endpoint, video, and streamed media.** The Voyage provider now targets MongoDB's Atlas Embedding and Reranking API as well as Voyage's own; an Atlas-prefixed key routes automatically, or pass `endpoint: :atlas` / `:voyage` explicitly. Adds `voyage-3.5`, `voyage-3.5-lite`, `voyage-code-2`, and `voyage-multimodal-3.5`, plus `embed_video`. `Parse::Embeddings::MediaFile` streams local image and video uploads into the request body in fixed-size chunks instead of buffering them, bounding peak memory regardless of file size. See [CHANGELOG.md](./CHANGELOG.md)
|
|
24
|
+
- **5.6.0 (Breaking): Voyage's default model moves to `voyage-3.5`.** `voyage-3` is retired from the Atlas endpoint, so the old default failed at construction for an Atlas key with no model named. Code relying on the previous default should pin `model: "voyage-3"` to keep existing embeddings valid, or re-embed against `voyage-3.5`. See [CHANGELOG.md](./CHANGELOG.md)
|
|
25
|
+
- **5.6.0: Vector search no longer underfills.** `$vectorSearch` applied its `limit` before the SDK's ACL match, `protectedFields` redaction, and pointer filtering, so a scoped caller who could read 2 of the top 10 documents got 2 results even when hundreds of readable matches existed further down the ranking. The search now requests a wider internal candidate window, applies every enforcement layer, and only then trims to the requested count; a `candidate_limit:` option tunes the window, and a `parse.vector_search.search` notification reports attrition. Hybrid search's own candidate-window and fusion-depth bugs are fixed the same way. See [CHANGELOG.md](./CHANGELOG.md)
|
|
26
|
+
- **5.6.0: `:vector` properties are checked against the provider actually registered.** A property's declared `model:` was recorded but never enforced, so swapping the registered provider's model could silently mix incomparable vectors into one index with no error. `Parse::Embeddings::BindingAudit` now checks `model:` and `dimensions:` before any request, on both the managed-write and query-embedding paths, and fails closed if the provider cannot report either. `Parse::Embeddings::BindingAudit.audit_all!` checks every declared binding at once for a boot-time or CI gate. See [CHANGELOG.md](./CHANGELOG.md)
|
|
27
|
+
- **5.6.0: Corrected the model dimension table.** The entire Voyage v4 family defaults to 1024; `voyage-4-large`'s 2048 and `voyage-4-lite`'s 512 were recorded as native widths when they are Matryoshka options reached only by requesting them, which made both models raise `Parse::Embeddings::InvalidResponseError` on every call. Any width on a model's Matryoshka ladder is now accepted. See [CHANGELOG.md](./CHANGELOG.md)
|
|
28
|
+
|
|
29
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full 5.6 entry.
|
|
30
|
+
|
|
7
31
|
## What's new in 5.5
|
|
8
32
|
|
|
9
33
|
- **5.5.0 — Multimodal bytes-fetch with magic-byte MIME verification** — `embed_image ..., source: :bytes` has the SDK download an image itself through the `Parse::File.safe_open_url` SSRF primitive, verify the content by **magic-byte sniff** (the `Content-Type` header is never consulted — a `.jpg` URL serving HTML is refused), cross-check the URL extension, enforce a `Parse::Embeddings.allowed_image_types` allowlist, strip EXIF/XMP metadata **by default** (JPEG APP1, PNG `eXIf`, WebP `EXIF`/`XMP ` chunks; opt out with `exif_strip: false`), and forward the verified bytes to Voyage/Cohere as a base64 data URI. No provider-side URL fetch occurs, so the `trust_provider_url_fetch` sentinel is not required — the host allowlist still applies. See [CHANGELOG.md](./CHANGELOG.md)
|
|
@@ -64,7 +88,7 @@ See [CHANGELOG.md](./CHANGELOG.md) for the full 5.1 entry, including breaking ch
|
|
|
64
88
|
- **MCP transport hardening** — Streamable HTTP `Mcp-Session-Id` header (renamed from `X-MCP-Session-Id`, **breaking**), `MCP-Protocol-Version` validation, `DELETE /` session termination, structured-content (`outputSchema`) on built-in tools, optional `health_path:` liveness probe
|
|
65
89
|
- **`Parse::GraphQL::TypeGenerator`** — generate `graphql-ruby` types directly from your `Parse::Object` subclasses (no Parse Server round-trip), with `:vector` columns surfaced as `[Float]` and association registries (`has_one_associations`, `has_many_associations`) populated at DSL time
|
|
66
90
|
- **LiveQuery promoted to stable** — the experimental warning is removed; `Parse.live_query_enabled = true` is retained as a network-egress safety toggle, not a stability gate
|
|
67
|
-
- **Server-version deprecation warning** — one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.
|
|
91
|
+
- **Server-version deprecation warning** — one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.10.0`); the default warning threshold is intentionally conservative so older deployments only get an advisory, not a hard break.
|
|
68
92
|
- **`mongo_relation_index :field, dedup: true`** — register a compound `{owningId, relatedId}` UNIQUE on relation join collections to prevent duplicate-pair subscriptions without breaking `has_many` semantics
|
|
69
93
|
|
|
70
94
|
See [CHANGELOG.md](./CHANGELOG.md) for the full 5.0 entry, including security-hardening notes and Ruby 3.x cleanup.
|
|
@@ -665,6 +689,271 @@ Redis is the recommended cache backend for multi-process / multi-dyno deployment
|
|
|
665
689
|
|
|
666
690
|
The cache surface is opt-in at two layers. Object fetches (`Model.find(id)`, `obj.reload!` in non-write-only mode) cache by default once a store is configured. Query results do **not** cache by default — pass `cache: true` per call (e.g. `Song.all(limit: 500, cache: true)`) or set `Parse.default_query_cache = true` for opt-out behavior. Both layers honor `cache: false` / `Cache-Control: no-cache` to skip the cache for an individual request.
|
|
667
691
|
|
|
692
|
+
[docs/caching.md](docs/caching.md) covers the whole picture: every cache the SDK keeps, which ones are process-local, how keys are scoped by auth and tenant, what invalidates what, and how to clear safely.
|
|
693
|
+
|
|
694
|
+
#### `:cache_keyspace`
|
|
695
|
+
|
|
696
|
+
Set `cache_keyspace: true` to place response, identity, and role-cache keys
|
|
697
|
+
inside a reserved, app-scoped layout:
|
|
698
|
+
|
|
699
|
+
```
|
|
700
|
+
parse-stack:v1:<app_scope>[:<namespace>]:<family>[:T:<tenant>]:<rest>
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
`app_scope` is a digest of your `application_id` and `server_url`, so two Parse
|
|
704
|
+
apps sharing one Redis stay separated even when neither sets a
|
|
705
|
+
`cache_namespace:`. The families are `cache` (the response cache), `idn`
|
|
706
|
+
(session-token identity), and `role` (role closures).
|
|
707
|
+
|
|
708
|
+
The keyspace is what makes clearing safe. Without it, `clear_cache!` falls
|
|
709
|
+
through to `FLUSHDB` whenever the `Parse::Cache::Redis` wrapper itself has no
|
|
710
|
+
namespace, which is the default. On a shared Redis that removes other tenants'
|
|
711
|
+
data, and it deletes the SDK's own `parse-stack:foc:v1:*` create-locks, so any
|
|
712
|
+
`first_or_create!` holding a lock at that moment silently loses its mutual
|
|
713
|
+
exclusion. With a keyspace configured, every clear is a scoped `SCAN` restricted
|
|
714
|
+
to this client's own keys. `flush_db!` stays available as the explicit opt-in
|
|
715
|
+
for a full flush.
|
|
716
|
+
|
|
717
|
+
The keyspace's response-key builder requires an auth discriminator, so a
|
|
718
|
+
master-key response and a session-token response for the same URL cannot share
|
|
719
|
+
an entry by accident. Enabling the keyspace also registers the webhook
|
|
720
|
+
invalidation triggers described below. Pass `cache_invalidation_hooks: false`
|
|
721
|
+
to skip the trigger registration.
|
|
722
|
+
|
|
723
|
+
This is opt-in and inert by default. With `cache_keyspace:` unset, the key
|
|
724
|
+
shape and every behavior above are unchanged from earlier releases.
|
|
725
|
+
|
|
726
|
+
```ruby
|
|
727
|
+
store = Parse::Cache::Redis.new(url: "redis://localhost:6379/0")
|
|
728
|
+
|
|
729
|
+
Parse.setup(
|
|
730
|
+
server_url: ENV.fetch("PARSE_SERVER_URL"),
|
|
731
|
+
application_id: ENV.fetch("PARSE_APP_ID"),
|
|
732
|
+
master_key: ENV.fetch("PARSE_MASTER_KEY"),
|
|
733
|
+
cache: store,
|
|
734
|
+
expires: 10,
|
|
735
|
+
cache_keyspace: true,
|
|
736
|
+
cache_namespace: "web", # optional, composes into the layout above
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# `Parse.setup` derives an immutable, per-client Parse::Cache::ScopedView
|
|
740
|
+
# from `store` and installs THAT as this client's SDK cache. `store` itself
|
|
741
|
+
# is never mutated: a keyspace can only ever be bound through a scoped view,
|
|
742
|
+
# so the same `store` can back several clients/apps without one's keyspace
|
|
743
|
+
# ever clobbering another's. Reach the scoped keyspace through
|
|
744
|
+
# `Parse.sdk_cache`; `Parse.cache` is still `store` itself, for your keys.
|
|
745
|
+
Parse.client.clear_cache! # scoped SCAN over this client's keys
|
|
746
|
+
Parse.sdk_cache.clear(family: :role) # one family only
|
|
747
|
+
Parse.sdk_cache.clear(family: :cache, tenant: "acme") # one tenant of one family
|
|
748
|
+
store.flush_db! # explicit full flush of the WHOLE backend, ops tooling only
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Scoped eviction uses `UNLINK` where the client supports it and emits a
|
|
752
|
+
`parse.cache.evict` `ActiveSupport::Notifications` event with
|
|
753
|
+
`pattern_digest`, `deleted`, and `duration_ms`.
|
|
754
|
+
|
|
755
|
+
#### Shared identity and role caches
|
|
756
|
+
|
|
757
|
+
`Parse::Authorization` resolves a session token to a user and a user to a role
|
|
758
|
+
closure for every scoped mongo-direct read, including Atlas Search's
|
|
759
|
+
`$search`. Each `Parse::Client` owns its own `Parse::Authorization::Context`
|
|
760
|
+
(`client.authorization`), so two clients addressing two Parse applications
|
|
761
|
+
never resolve a token against each other's caches or each other's
|
|
762
|
+
`/users/me`. Both resolutions are cached in per-process memory by default, so
|
|
763
|
+
each Puma worker and each dyno maintains its own copy. Once
|
|
764
|
+
`cache_keyspace: true` is set, the client's scoped view
|
|
765
|
+
(`Parse.client.sdk_cache`) exposes two planes that drop into those slots and
|
|
766
|
+
move the caches to the shared backend:
|
|
767
|
+
|
|
768
|
+
```ruby
|
|
769
|
+
# `Parse.client.sdk_cache` is the scoped view the client derived at setup. The
|
|
770
|
+
# planes live on the view rather than on the backend, so two clients sharing
|
|
771
|
+
# one Redis connection cannot end up sharing each other's caches.
|
|
772
|
+
view = Parse.client.sdk_cache
|
|
773
|
+
Parse::Authorization.configure(
|
|
774
|
+
identity_cache: view.identity(ttl: 3600),
|
|
775
|
+
role_cache: view.roles(ttl: 30),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# A named client pointed at a second Parse application configures its own
|
|
779
|
+
# context directly. Parse::Authorization.configure only ever reaches the
|
|
780
|
+
# default client, by design: there is no such thing as "the" client below
|
|
781
|
+
# that boundary.
|
|
782
|
+
other_view = other_client.sdk_cache
|
|
783
|
+
other_client.authorization.configure(
|
|
784
|
+
identity_cache: other_view.identity(ttl: 3600),
|
|
785
|
+
role_cache: other_view.roles(ttl: 30),
|
|
786
|
+
)
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Each plane writes inside its own keyspace family, so clearing one reaches
|
|
790
|
+
neither the other nor the response cache. Both require `cache_keyspace: true`.
|
|
791
|
+
`Parse::AtlasSearch.session_cache=` and `.role_cache=` remain as deprecated
|
|
792
|
+
aliases for the default client's context and are slated for removal in 6.0.
|
|
793
|
+
|
|
794
|
+
Staleness is bounded by webhook rather than by application code.
|
|
795
|
+
`Parse::Cache::Invalidation` registers `after_save` and `after_delete` on
|
|
796
|
+
`_Role` and `_User` plus `after_logout` on `_Session`, so a role change made by
|
|
797
|
+
any client (a mobile SDK, the dashboard, Node cloud code) invalidates the planes
|
|
798
|
+
the same way an SDK write does. This requires the webhook endpoint to be
|
|
799
|
+
registered and reachable from Parse Server; where it is not, the TTL remains the
|
|
800
|
+
only bound on staleness.
|
|
801
|
+
|
|
802
|
+
#### Client-owned authorization meets a process-global MongoDB connection
|
|
803
|
+
|
|
804
|
+
Authorization is per client as of 5.7. The MongoDB connection is not:
|
|
805
|
+
`Parse::MongoDB.configure` sets one URI, one database, and one driver client
|
|
806
|
+
for the whole process, and that stays true until 6.0. Each half is safe on its
|
|
807
|
+
own and the combination is not.
|
|
808
|
+
|
|
809
|
+
A second client would resolve its session token correctly, against its own
|
|
810
|
+
Parse application, and build a correct `_rperm` allow-set for one of its users.
|
|
811
|
+
The resulting pipeline would then run against the *other* application's
|
|
812
|
+
database, where those user ids and role names are matched against rows they
|
|
813
|
+
have nothing to do with. Any collision is a cross-application read, and
|
|
814
|
+
nothing about it looks like a failure. It looks like a query that returned few
|
|
815
|
+
rows.
|
|
816
|
+
|
|
817
|
+
`Parse::MongoDB` therefore records the application it was configured for, and
|
|
818
|
+
`Parse::MongoDB.verify_client!` refuses any mongo-direct query whose
|
|
819
|
+
authorization came from a client belonging to a different one, raising
|
|
820
|
+
`Parse::MongoDB::ClientMismatch`. The check runs in two places, because
|
|
821
|
+
`aggregate` is not the only way to reach the database: Atlas Search builds and
|
|
822
|
+
runs its own `$search` pipelines, and hybrid vector search does the same, both
|
|
823
|
+
going straight to the driver. Both verify against the client that authorized
|
|
824
|
+
the call: `aggregate` passes the one it resolved, and
|
|
825
|
+
`Parse::MongoDB.collection` takes an `authorizing_client:` that each of those
|
|
826
|
+
paths forwards. `collection` does NOT substitute the default for a missing
|
|
827
|
+
one, because doing so would present a call site that forgot to forward its
|
|
828
|
+
client as the default and wave it through, which is the omission the check
|
|
829
|
+
exists to catch. Public entry points resolve an omitted client to the default
|
|
830
|
+
once, and internal sinks treat nil as unidentified.
|
|
831
|
+
|
|
832
|
+
`results_direct`, `count_direct`, `distinct_direct`,
|
|
833
|
+
`distinct_direct_pointers`, and `Parse::MongoDB.aggregate` all take `client:`
|
|
834
|
+
alongside the other auth keywords. It selects the authorization context that
|
|
835
|
+
resolves the call, and it is what the guard compares against the binding:
|
|
836
|
+
|
|
837
|
+
```ruby
|
|
838
|
+
Parse.setup(application_id: "appA", ...)
|
|
839
|
+
Parse::MongoDB.configure(uri: ENV.fetch("DATABASE_URI")) # bound to appA
|
|
840
|
+
|
|
841
|
+
other = Parse::Client.new(application_id: "appB", ...)
|
|
842
|
+
|
|
843
|
+
# Resolves appB's token against appB, then would read appA's database. Refused.
|
|
844
|
+
Post.query.results_direct(session_token: token, client: other)
|
|
845
|
+
# => Parse::MongoDB::ClientMismatch
|
|
846
|
+
|
|
847
|
+
# A query can carry the client instead of repeating it per call. Assign it;
|
|
848
|
+
# `Post.query(client: other)` would build a constraint on a field named
|
|
849
|
+
# `client` and match nothing.
|
|
850
|
+
q = Post.query
|
|
851
|
+
q.client = other
|
|
852
|
+
q.results_direct(session_token: token)
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
It fires the same way when the default client is replaced after MongoDB was
|
|
856
|
+
configured, which needs no explicit `client:` at all:
|
|
857
|
+
|
|
858
|
+
```ruby
|
|
859
|
+
Parse.setup(application_id: "appB", ...) # default is now appB, binding is appA
|
|
860
|
+
Post.query.results_direct(session_token: token)
|
|
861
|
+
# => Parse::MongoDB::ClientMismatch
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Omitting `client:` resolves through `Parse.client`, which is the existing
|
|
865
|
+
behavior and what every single-application deployment gets.
|
|
866
|
+
|
|
867
|
+
The binding comes from the default client, which is the one whose
|
|
868
|
+
configuration produced the connection. It is established at `configure` time,
|
|
869
|
+
or on first use when MongoDB was configured before `Parse.setup`, so that boot
|
|
870
|
+
order does not leave the check disabled. It is never taken from whichever
|
|
871
|
+
caller happens to arrive first: in a process that configures MongoDB early,
|
|
872
|
+
installs default client A, then makes its first direct request explicitly as
|
|
873
|
+
B, the connection belongs to A and B's read is refused.
|
|
874
|
+
|
|
875
|
+
A caller that cannot name its client is treated as unidentified, never
|
|
876
|
+
upgraded to the default. What happens next depends on how many applications
|
|
877
|
+
the process has seen:
|
|
878
|
+
|
|
879
|
+
- **One application.** Unidentified callers proceed. This is every
|
|
880
|
+
single-application deployment, and it includes master-mode and
|
|
881
|
+
public-fallback resolutions produced before `Parse.setup`.
|
|
882
|
+
- **Two or more.** Unidentified callers are refused. A call path that fails to
|
|
883
|
+
forward its client fails loudly rather than silently reading whichever
|
|
884
|
+
database happened to be bound, so the completeness of that plumbing is not
|
|
885
|
+
the only thing keeping two applications apart.
|
|
886
|
+
|
|
887
|
+
The owning client counts toward that tally even if it never issues a direct
|
|
888
|
+
read itself, so a process where only the second application ever identifies
|
|
889
|
+
itself still counts as two.
|
|
890
|
+
|
|
891
|
+
If you genuinely need two applications in one process, give each its own
|
|
892
|
+
process, or route the second one's reads through REST, where Parse Server
|
|
893
|
+
enforces ACLs itself. The guard becomes unnecessary in 6.0, when the
|
|
894
|
+
connection becomes client-owned.
|
|
895
|
+
|
|
896
|
+
#### `:parse_cache_url`
|
|
897
|
+
|
|
898
|
+
Parse Server keeps its own role cache, writing the transitive closure for a user
|
|
899
|
+
as `<appId>:role:<userId>`. Pointing `Parse::Cache::Redis` at it lets the SDK
|
|
900
|
+
compare that value with its own role-graph result, or lets trusted application
|
|
901
|
+
code read it explicitly when a webhook payload already supplies a user id.
|
|
902
|
+
|
|
903
|
+
```ruby
|
|
904
|
+
store = Parse::Cache::Redis.new(
|
|
905
|
+
url: "redis://localhost:6379/0", # the SDK's own cache
|
|
906
|
+
parse_cache_url: "redis://localhost:6379/1", # Parse Server's cache, read-only
|
|
907
|
+
)
|
|
908
|
+
Parse.setup(cache: store, cache_keyspace: true, ...)
|
|
909
|
+
view = Parse.client.sdk_cache
|
|
910
|
+
|
|
911
|
+
# true (isolated), false (shared, and warned about), or :unknown.
|
|
912
|
+
store.verify_upstream_isolation!
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**The two URLs must address different Redis databases.** On Parse Server 9.10.0
|
|
916
|
+
and earlier, a `_Role` write clears the cache with `FLUSHDB`, which on a shared
|
|
917
|
+
database deletes the SDK's cached responses and its create-locks along with it
|
|
918
|
+
([parse-server#10617](https://github.com/parse-community/parse-server/issues/10617)).
|
|
919
|
+
`verify_upstream_isolation!` detects this by scanning the SDK's own database
|
|
920
|
+
for a key shaped like one Parse Server would have written, and, when that finds
|
|
921
|
+
nothing, by writing a random sentinel to the SDK's database and asking the
|
|
922
|
+
upstream connection to read it back. The scan alone can only prove sharing: an
|
|
923
|
+
empty result looks identical on a separate database and on a shared one where
|
|
924
|
+
Parse Server has not cached a role closure yet. It returns `true` for
|
|
925
|
+
established isolation, `false` for established sharing, and `:unknown` when
|
|
926
|
+
neither could be shown, which is what the restricted credential below produces
|
|
927
|
+
since the sentinel read is denied. `:unknown` is truthy. Comparing URL strings
|
|
928
|
+
cannot substitute for any of this: `localhost` against `127.0.0.1`, CNAMEs,
|
|
929
|
+
Sentinel and Cluster topologies, and a database selected outside the URL all
|
|
930
|
+
defeat it. It warns rather than refusing to boot, because the hazard disappears
|
|
931
|
+
on a server carrying the scoped-clear fix.
|
|
932
|
+
|
|
933
|
+
Role resolution never consumes the upstream value. `Parse::Authorization`
|
|
934
|
+
computes its own closure, and the only built-in integration is
|
|
935
|
+
`compare_upstream_roles`, which reads the upstream entry solely to emit a
|
|
936
|
+
`parse.cache.role_compare` event. Call `view.upstream_roles.roles_for(user_id)`
|
|
937
|
+
yourself to use the value.
|
|
938
|
+
|
|
939
|
+
The attachment is strictly read-only. The SDK never writes that keyspace: its
|
|
940
|
+
own closure is depth-capped while Parse Server's is not, so writing a subset
|
|
941
|
+
into a cache the server treats as authoritative would under-permission users.
|
|
942
|
+
Every failure mode (a miss, malformed JSON, an entry whose remaining TTL cannot
|
|
943
|
+
be read or is implausibly long, an entry older than the SDK's last role
|
|
944
|
+
invalidation, a transport error) degrades to a miss and the closure is
|
|
945
|
+
recomputed. It never fails open.
|
|
946
|
+
|
|
947
|
+
If application code consumes those role names for authorization, that database
|
|
948
|
+
becomes part of its trust base. Restrict the credential accordingly:
|
|
949
|
+
|
|
950
|
+
```
|
|
951
|
+
ACL SETUSER parse-stack-role-reader on >SECRET \
|
|
952
|
+
~<appId>:role:* resetchannels -@all +get +pttl
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
`+pttl` is required alongside `+get`; the freshness guard cannot run without it.
|
|
956
|
+
|
|
668
957
|
#### `:expires`
|
|
669
958
|
Sets the default cache expiration time (in seconds) for successful non-empty `GET` requests when using the caching middleware. The default value is 3 seconds. If `:expires` is set to 0, caching will be disabled. You can always clear the current state of the cache using the `clear_cache!` method on your `Parse::Client` instance.
|
|
670
959
|
|
|
@@ -3450,6 +3739,33 @@ Song.query(cache: true).first # Explicitly uses cache
|
|
|
3450
3739
|
You may access the shared cache for the default client connection through `Parse.cache`. This is useful if you
|
|
3451
3740
|
want to utilize the same cache store for other purposes.
|
|
3452
3741
|
|
|
3742
|
+
`Parse.cache` is exactly the store you configured, and it is not affected by
|
|
3743
|
+
`cache_keyspace: true`, which only scopes the SDK's own slice. That slice is
|
|
3744
|
+
`Parse.sdk_cache`.
|
|
3745
|
+
|
|
3746
|
+
**`clear_cache!` only spares your keys when `cache_keyspace: true` is set.**
|
|
3747
|
+
Without it there is no keyspace to confine the clear to, `sdk_cache` and
|
|
3748
|
+
`cache` are the same object, and `clear_cache!` falls back to the store's own
|
|
3749
|
+
`clear`. What that reaches depends on the store, so there are two axes, not
|
|
3750
|
+
one:
|
|
3751
|
+
|
|
3752
|
+
For `Parse::Cache::Redis`, which is the only store that implements a
|
|
3753
|
+
namespace:
|
|
3754
|
+
|
|
3755
|
+
| | no `namespace:` | `namespace: "web"` |
|
|
3756
|
+
|---|---|---|
|
|
3757
|
+
| **no `cache_keyspace`** | clears everything on the store | clears `web:*` only |
|
|
3758
|
+
| **`cache_keyspace: true`** | clears the SDK keyspace only | clears the SDK keyspace only |
|
|
3759
|
+
|
|
3760
|
+
For any other Moneta store there is no middle column. `cache_namespace:` is a
|
|
3761
|
+
key-prefix option for the SDK's own keys and does not make the store's `clear`
|
|
3762
|
+
selective, so without `cache_keyspace: true` a `clear_cache!` takes the whole
|
|
3763
|
+
store regardless of any namespace you set.
|
|
3764
|
+
|
|
3765
|
+
Turn `cache_keyspace: true` on before relying on the separation. A Redis
|
|
3766
|
+
namespace narrows the blast radius but is not the same guarantee: it still
|
|
3767
|
+
takes any of your own keys that happen to sit under the same prefix.
|
|
3768
|
+
|
|
3453
3769
|
```ruby
|
|
3454
3770
|
# Access the cache instance for other uses
|
|
3455
3771
|
Parse.cache["key"] = "value"
|
|
@@ -3464,6 +3780,38 @@ end
|
|
|
3464
3780
|
|
|
3465
3781
|
```
|
|
3466
3782
|
|
|
3783
|
+
**`Parse.cache` versus `Parse.sdk_cache`.** The two answer different questions
|
|
3784
|
+
and only one of them is yours.
|
|
3785
|
+
|
|
3786
|
+
| | `Parse.cache` | `Parse.sdk_cache` |
|
|
3787
|
+
|---|---|---|
|
|
3788
|
+
| What it is | the store you configured, unchanged | the SDK's slice of it |
|
|
3789
|
+
| Key names | the keys you write | prefixed with the SDK keyspace |
|
|
3790
|
+
| Scoped by `cache_keyspace: true` | no | yes |
|
|
3791
|
+
| What `clear_cache!` clears (keyspaced) | nothing | this |
|
|
3792
|
+
| What `clear_cache!` clears (not keyspaced) | the store's own `clear`, they are the same object | |
|
|
3793
|
+
|
|
3794
|
+
`clear_cache!` operates on `sdk_cache`. With a keyspace that is only the SDK's
|
|
3795
|
+
response-cache entries and its identity and role planes. Without one, see the
|
|
3796
|
+
caveat above.
|
|
3797
|
+
|
|
3798
|
+
`Parse.cache.clear` is a different thing entirely and stays available on
|
|
3799
|
+
purpose, but what it reaches depends on how the store was built:
|
|
3800
|
+
|
|
3801
|
+
- **No `namespace:`** it clears the whole store. On `Parse::Cache::Redis` that
|
|
3802
|
+
is `FLUSHDB`: your keys, the SDK's, any other application on that database,
|
|
3803
|
+
and the `parse-stack:foc:v1:*` create-locks whose loss silently drops
|
|
3804
|
+
`first_or_create!` mutual exclusion.
|
|
3805
|
+
- **With `namespace:`** it scan-deletes `<namespace>:*` only, so keys outside
|
|
3806
|
+
that prefix survive.
|
|
3807
|
+
|
|
3808
|
+
`Parse::Cache::Redis#flush_db!` is the unconditionally total operation. Reach
|
|
3809
|
+
for either only when you own the whole database.
|
|
3810
|
+
|
|
3811
|
+
For anything beyond incidental use, give your application its own separately
|
|
3812
|
+
named Moneta instance rather than sharing the SDK's. Sharing one store means
|
|
3813
|
+
sharing its eviction policy and its failure modes.
|
|
3814
|
+
|
|
3467
3815
|
#### :use_master_key
|
|
3468
3816
|
A true/false value. If you provided a master key as part of `Parse.setup()`, it will be sent on every request. However, if you wish to disable sending the master key on a particular request in order for the record ACLs to be enforced, you may pass `false`. If `false` is passed, caching will be disabled for this request.
|
|
3469
3817
|
|
|
@@ -5726,17 +6074,37 @@ result = Parse::AtlasSearch.search("Song", "love", master: true)
|
|
|
5726
6074
|
# missing-auth call an `ACLRequired` error instead.
|
|
5727
6075
|
```
|
|
5728
6076
|
|
|
5729
|
-
Caching for session-token lookups is configurable
|
|
6077
|
+
Caching for session-token lookups is configurable through
|
|
6078
|
+
`Parse::Authorization`, which owns resolution for every client, not just Atlas
|
|
6079
|
+
Search:
|
|
5730
6080
|
|
|
5731
6081
|
```ruby
|
|
5732
|
-
Parse
|
|
5733
|
-
Parse
|
|
6082
|
+
Parse.client.authorization.identity_cache_ttl = 3600 # token → user_id
|
|
6083
|
+
Parse.client.authorization.role_cache_ttl = 120 # user_id → role names
|
|
5734
6084
|
|
|
5735
6085
|
# Force re-resolution after logout / role mutation:
|
|
5736
|
-
Parse
|
|
5737
|
-
Parse
|
|
6086
|
+
Parse.client.authorization.invalidate(token)
|
|
6087
|
+
Parse.client.authorization.invalidate_user_roles(user_id)
|
|
5738
6088
|
```
|
|
5739
6089
|
|
|
6090
|
+
Both caches default to per-process memory. With a keyspaced
|
|
6091
|
+
`Parse::Cache::Redis` you can move them to the shared backend so every worker
|
|
6092
|
+
resolves against the same view, and let webhook triggers invalidate them
|
|
6093
|
+
instead of calling `invalidate` from your own logout and role-mutation paths:
|
|
6094
|
+
|
|
6095
|
+
```ruby
|
|
6096
|
+
# `Parse.client.sdk_cache` is the scoped view the client derived at setup. The
|
|
6097
|
+
# planes live on the view rather than on the backend, so two clients sharing
|
|
6098
|
+
# one Redis connection cannot end up sharing each other's caches.
|
|
6099
|
+
view = Parse.client.sdk_cache
|
|
6100
|
+
Parse::Authorization.configure(
|
|
6101
|
+
identity_cache: view.identity(ttl: 3600),
|
|
6102
|
+
role_cache: view.roles(ttl: 30),
|
|
6103
|
+
)
|
|
6104
|
+
```
|
|
6105
|
+
|
|
6106
|
+
See [Shared identity and role caches](#shared-identity-and-role-caches).
|
|
6107
|
+
|
|
5740
6108
|
Notes:
|
|
5741
6109
|
|
|
5742
6110
|
- `faceted_search` cannot ACL-filter `$searchMeta` bucket counts and
|
|
@@ -5747,6 +6115,10 @@ Notes:
|
|
|
5747
6115
|
direction: a user's permissions include any role whose `roles`
|
|
5748
6116
|
relation transitively contains a role the user directly belongs
|
|
5749
6117
|
to. See `Parse::Role.all_for_user` for the primitive.
|
|
6118
|
+
- `Parse::AtlasSearch.session_cache_ttl=`, `.role_cache_ttl=`, and
|
|
6119
|
+
`Parse::AtlasSearch::Session.invalidate` / `.invalidate_user_roles` still
|
|
6120
|
+
work. They delegate to the default client's `Parse::Authorization::Context`
|
|
6121
|
+
and are slated for removal in 6.0.
|
|
5750
6122
|
|
|
5751
6123
|
### Full-Text Search
|
|
5752
6124
|
|