parse-stack-next 5.6.0 → 5.7.0
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 +409 -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 +167 -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/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 +97 -15
- 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 +2 -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 +2 -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 +132 -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 +22 -4
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a554fcec1f40152e03956f033d593c1a4a98fa09ef22e033c270721c701d336
|
|
4
|
+
data.tar.gz: 10db25291fc3e2f1e61a36df7499f5b38af2e4e04f73f72f7adfaca8c55dd156
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa3333741642e2faea789f829e23c37f3b72dccba2a4c5ed85b3601d2a945a58767cc65cd55ab9eb385225cf6d61cccbe0e4fb824891c273bc735c3c792916e5
|
|
7
|
+
data.tar.gz: 572f5a1bd5683d2e9a7aad5293e279401680e268359e3d9f805875453a92ac4ca570ac9c4da9a7c85542b986b91544fcf6adffe223314af0aef7a3828071f7cf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,414 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.7.0
|
|
4
|
+
|
|
5
|
+
#### Cache keys move into a reserved, app-scoped keyspace
|
|
6
|
+
|
|
7
|
+
- **NEW**: `Parse::Cache::Keyspace` owns the physical layout of the response,
|
|
8
|
+
identity, and role-cache keys the SDK writes to a shared cache backend along
|
|
9
|
+
with the glob patterns that clear them again, so key generation and eviction
|
|
10
|
+
can no longer drift apart. Keys are laid out as
|
|
11
|
+
`parse-stack:v1:<app_scope>[:<namespace>]:<family>[:T:<tenant>]:<rest>`, with
|
|
12
|
+
`cache`, `idn`, and `role` as the families. `app_scope` is a digest of the
|
|
13
|
+
application id and the server URL rather than the raw values, so two apps
|
|
14
|
+
sharing one Redis no longer collide when neither sets a `cache_namespace:`,
|
|
15
|
+
and an application id carrying a glob metacharacter cannot silently widen a
|
|
16
|
+
SCAN pattern. Enable it with `cache_keyspace: true` on `Parse.setup`. Without
|
|
17
|
+
that option the previous key shape and behavior are preserved exactly, so the
|
|
18
|
+
upgrade is inert until an operator opts in.
|
|
19
|
+
- **FIXED**: `Parse::Client#clear_cache!` no longer reaches `FLUSHDB` **when a
|
|
20
|
+
keyspace is configured**. The wrapper fell through to a full flush whenever no
|
|
21
|
+
namespace was set on it, which is the default, and the caching middleware
|
|
22
|
+
could hold a `cache_namespace:` the wrapper knew nothing about. On a shared
|
|
23
|
+
Redis that destroyed co-tenant data, and it deleted the SDK's own
|
|
24
|
+
`parse-stack:foc:v1:*` create-locks, removing `first_or_create!` mutual
|
|
25
|
+
exclusion for any lock held at the time with no error anywhere. With
|
|
26
|
+
`cache_keyspace: true`, every clear is a scoped SCAN inside the client's own
|
|
27
|
+
keys and can only ever delete a subset of them, and `scope:` narrows within
|
|
28
|
+
that keyspace rather than widening past it. `flush_db!` remains the explicit
|
|
29
|
+
opt-in for a full flush.
|
|
30
|
+
|
|
31
|
+
**Without `cache_keyspace: true` the old behavior is unchanged**, including
|
|
32
|
+
the `FLUSHDB` fallback when no namespace is set, and a plain Moneta store
|
|
33
|
+
passed as `cache:` is still cleared in full because it has no notion of key
|
|
34
|
+
scoping. Opting in is what fixes it. This is stated plainly because a reader
|
|
35
|
+
who upgrades and changes nothing else is still exposed.
|
|
36
|
+
- **NEW**: The keyspace-bound `Parse::Cache::ScopedView` exposed as
|
|
37
|
+
`client.sdk_cache` accepts `family:` and `tenant:` on `#clear`, and
|
|
38
|
+
`#delete_matching(pattern)` evicts by glob. A pattern outside the view's own
|
|
39
|
+
keyspace is a no-op rather than an unscoped scan, so the narrower API cannot
|
|
40
|
+
become a back door to the blast radius the keyspace exists to close.
|
|
41
|
+
- **CHANGED**: Scoped eviction issues `UNLINK` rather than `DEL` when the
|
|
42
|
+
client exposes it, so reclaiming a large eviction runs on a Redis background
|
|
43
|
+
thread instead of stalling the server, falling back to `DEL` on older
|
|
44
|
+
clients. Each eviction emits a `parse.cache.evict`
|
|
45
|
+
`ActiveSupport::Notifications` event carrying `pattern_digest`, `deleted`,
|
|
46
|
+
and `duration_ms`, so an operator can see how much a clear actually removed
|
|
47
|
+
and how long it took. The pattern is digested rather than logged because it
|
|
48
|
+
embeds a URL digest and a cache tenant.
|
|
49
|
+
- **CHANGED**: `Parse::Cache::Redis` refuses a Moneta `prefix:` option. It
|
|
50
|
+
rewrites the physical key layout underneath the wrapper, which would break
|
|
51
|
+
every SCAN pattern the class builds and quietly restore the unscoped clearing
|
|
52
|
+
the keyspace exists to prevent. Use `cache_namespace:` instead.
|
|
53
|
+
|
|
54
|
+
#### The response cache's auth separation is now enforced by construction
|
|
55
|
+
|
|
56
|
+
- **IMPROVED**: The auth discriminator on a response-cache key is now
|
|
57
|
+
structural rather than incidental. A master-key request bypasses ACL, CLP, and
|
|
58
|
+
`protectedFields`, so for the same URL it returns a strictly fuller body than a
|
|
59
|
+
session-token request, and two session-token requests can differ from each
|
|
60
|
+
other through `protectedFields` entity rules and row ACLs. Previous versions
|
|
61
|
+
already separated these, prefixing the key with `mk:` or a token digest, so
|
|
62
|
+
this is not a fix for a cache that crossed those boundaries. What changes is
|
|
63
|
+
that the separation can no longer be lost by accident:
|
|
64
|
+
`Parse::Cache::Keyspace#cache_key` has no default for `auth:` and raises
|
|
65
|
+
rather than building a key without one, and the generic key builder refuses
|
|
66
|
+
the response-cache family outright. A raw session token is still never
|
|
67
|
+
accepted as a key segment, only a truncated digest.
|
|
68
|
+
- **FIXED**: A non-GET write now invalidates the resource for every caller. The
|
|
69
|
+
previous invalidation could name only the anonymous variant, the master-key
|
|
70
|
+
variant, and the caller's own, and had no way to enumerate the entries of
|
|
71
|
+
sessions the process had never seen, so a write by one user left every other
|
|
72
|
+
user reading a stale copy until the TTL expired. Every auth variant of one
|
|
73
|
+
resource now shares a key prefix, so a scan-capable store evicts all of them
|
|
74
|
+
with a single pattern. A GET miss still clears only the siblings it can name,
|
|
75
|
+
since evicting resource-wide there would destroy other sessions' valid
|
|
76
|
+
entries on every cache miss.
|
|
77
|
+
- **CHANGED**: Invalidation also deletes the pre-keyspace form of a key, so a
|
|
78
|
+
rolling deploy does not leave old workers serving entries that a new worker's
|
|
79
|
+
write should have killed. Pass `cache_delete_legacy_variants: false` to stop
|
|
80
|
+
once every old worker is drained.
|
|
81
|
+
|
|
82
|
+
#### Identity and role caching share one backend across processes
|
|
83
|
+
|
|
84
|
+
- **NEW**: `Parse::Cache::ScopedView#identity` and `#roles` return
|
|
85
|
+
`Parse::Cache::SubCache` planes shaped for a client's
|
|
86
|
+
`Parse::Authorization::Context#identity_cache` and `#role_cache` slots. The
|
|
87
|
+
keyspace-bound view is exposed as `client.sdk_cache`; installing its planes
|
|
88
|
+
replaces the default per-process memory caches with a shared backend, so
|
|
89
|
+
every Puma worker and every dyno resolves a session token or a role closure
|
|
90
|
+
against the same view instead of each holding its own. Each plane writes
|
|
91
|
+
inside its own keyspace family, so clearing one can reach neither the other
|
|
92
|
+
nor the response cache.
|
|
93
|
+
- **NEW**: Each plane carries a per-subject generation counter and a plane-wide
|
|
94
|
+
epoch, both for invalidating entries that cannot be named. Identity entries
|
|
95
|
+
are keyed by session token and no reverse map from user id exists, so a
|
|
96
|
+
`_User` write cannot enumerate them. Bumping that user's generation
|
|
97
|
+
invalidates all of them in constant time, including tokens the process has
|
|
98
|
+
never resolved, and without a master-key `_Session` query. The epoch answers
|
|
99
|
+
the different question of whether an entry written before a given moment is
|
|
100
|
+
stale, which is what judging a foreign cache entry requires. It never moves
|
|
101
|
+
backwards, so clock skew between workers cannot re-admit an entry a previous
|
|
102
|
+
invalidation had rejected.
|
|
103
|
+
|
|
104
|
+
#### Role and session invalidation no longer depends on application discipline
|
|
105
|
+
|
|
106
|
+
- **NEW**: `Parse::Cache::Invalidation` registers the webhook triggers that
|
|
107
|
+
keep the identity and role planes honest: `after_save` and `after_delete` on
|
|
108
|
+
`_Role`, `after_save` and `after_delete` on `_User`, and `after_logout` on
|
|
109
|
+
`_Session`. It installs alongside the keyspace and is disabled with
|
|
110
|
+
`cache_invalidation_hooks: false`. The previous contract asked applications
|
|
111
|
+
to call `Parse::AtlasSearch::Session.invalidate` and `invalidate_user_roles`
|
|
112
|
+
from their own logout and role-mutation paths. That depended on every
|
|
113
|
+
application remembering, and it missed role changes made by any other client,
|
|
114
|
+
including a mobile SDK, the dashboard, and Node cloud code. The triggers
|
|
115
|
+
cover writes from every source Parse Server sees. TTL remains the backstop,
|
|
116
|
+
since the triggers require a webhook endpoint Parse Server can reach and
|
|
117
|
+
hooks registered against it: this is TTL and hooks, not TTL or hooks.
|
|
118
|
+
- **FIXED**: Registering a webhook handler replaced any handler already
|
|
119
|
+
registered for the same trigger instead of composing with it, for every
|
|
120
|
+
trigger except `after_save` and `after_delete`. A second `after_logout`
|
|
121
|
+
registration silently discarded the first, with file load order deciding the
|
|
122
|
+
winner and nothing warning about it. Non-rejectable `after_*` triggers now
|
|
123
|
+
accumulate handlers the way `after_save` always has. Rejectable `before_*`
|
|
124
|
+
triggers deliberately keep replacing: a composite of those must deny if any
|
|
125
|
+
handler denies, and folding the results with `.last` would discard an earlier
|
|
126
|
+
rejection.
|
|
127
|
+
|
|
128
|
+
#### Optional read of Parse Server's own role cache
|
|
129
|
+
|
|
130
|
+
- **NEW**: `Parse::Cache::UpstreamRoles` reads the `<appId>:role:<userId>`
|
|
131
|
+
closure Parse Server writes for itself, so a caller holding a trusted user
|
|
132
|
+
id, most usefully from a webhook payload, can skip the role-graph walk by
|
|
133
|
+
calling `client.sdk_cache.upstream_roles.roles_for`. Attach it by passing
|
|
134
|
+
`parse_cache_url:` to `Parse::Cache::Redis`. Without that option nothing
|
|
135
|
+
upstream is read.
|
|
136
|
+
- **NEW**: Role resolution itself does not consume the upstream value in this
|
|
137
|
+
release. `Parse::AtlasSearch::Session` always computes its own closure, and
|
|
138
|
+
the only built-in integration is `compare_upstream_roles`, which reads the
|
|
139
|
+
upstream entry purely to emit a `parse.cache.role_compare` event carrying
|
|
140
|
+
the size of each set and their symmetric difference. Nothing about the ACL
|
|
141
|
+
decision changes. This is deliberate: the upstream value becomes an
|
|
142
|
+
authorization input the moment it is consumed, so it stays observable-only
|
|
143
|
+
until the two closures have been reconciled against real traffic. The
|
|
144
|
+
comparison is inert unless both the switch and a reader are set.
|
|
145
|
+
- **NEW**: The attachment is strictly read-only. The SDK never writes that
|
|
146
|
+
keyspace, because its own closure is depth-capped while Parse Server's is
|
|
147
|
+
not, so injecting a strict subset into a cache the server reads back as
|
|
148
|
+
authoritative would under-permission users in windows that are close to
|
|
149
|
+
undiagnosable.
|
|
150
|
+
- **NEW**: Every failure mode degrades to a miss so the caller recomputes the
|
|
151
|
+
closure, and none of them fails open. The decoded value must be a JSON array
|
|
152
|
+
of `role:`-prefixed names within the configured count and length caps. An
|
|
153
|
+
entry whose remaining PTTL cannot be read, is negative, or exceeds the
|
|
154
|
+
configured ceiling is rejected, because an entry whose age cannot be derived
|
|
155
|
+
is not one to trust as an authorization input. An entry written before the
|
|
156
|
+
SDK's last role invalidation is rejected by the plane epoch, because Parse
|
|
157
|
+
Server does not clear its own role cache on a `_Role` delete. The reader
|
|
158
|
+
needs only `+get` and `+pttl`, so the credential can be restricted to the
|
|
159
|
+
role keyspace.
|
|
160
|
+
- **NEW**: `Parse::Cache::Redis#verify_upstream_isolation!` probes whether the
|
|
161
|
+
two endpoints resolve to the same Redis database, first by scanning the
|
|
162
|
+
SDK's own database for a key shaped like one Parse Server would have
|
|
163
|
+
written, then, if that finds nothing, by writing a random sentinel to the
|
|
164
|
+
SDK's database and asking the upstream connection to read it back. The scan
|
|
165
|
+
alone can only ever prove sharing: an empty result is equally consistent
|
|
166
|
+
with a separate database and with a shared one on which Parse Server has
|
|
167
|
+
not yet cached a role closure, which is the state of every freshly deployed
|
|
168
|
+
stack. Only the sentinel establishes the negative. The method returns
|
|
169
|
+
`true` for established isolation, `false` for established sharing, and
|
|
170
|
+
`:unknown` when neither could be shown, which is what a credential
|
|
171
|
+
restricted to `~<appId>:role:*` produces: the sentinel read is denied, and
|
|
172
|
+
a denial says nothing about which database denied it. `:unknown` is truthy,
|
|
173
|
+
so callers branching on truthiness are unaffected. Comparing URLs would be
|
|
174
|
+
defeated by `localhost` against `127.0.0.1`, by CNAMEs, by Sentinel and
|
|
175
|
+
Cluster topologies, and by a database selected outside the URL. A shared
|
|
176
|
+
database is a real hazard: on Parse Server 9.10.0 and earlier a `_Role` write
|
|
177
|
+
clears the cache with `FLUSHDB`, which takes the SDK's cached responses and
|
|
178
|
+
its `first_or_create!` create-locks with it. See
|
|
179
|
+
https://github.com/parse-community/parse-server/issues/10617. The probe warns
|
|
180
|
+
rather than refusing to boot, since the hazard disappears entirely on a
|
|
181
|
+
server carrying the scoped-clear fix.
|
|
182
|
+
|
|
183
|
+
#### Role graph queries now accept the public API's default depth
|
|
184
|
+
|
|
185
|
+
- **CHANGED**: Raised the MongoDB role-graph query default and hard cap from 6
|
|
186
|
+
to 10, matching the existing `Parse::Role.all_for_user` default. A ceiling
|
|
187
|
+
below that default made the opt-in MongoDB fast path raise `ArgumentError`
|
|
188
|
+
for any caller who did not pass an explicit `max_depth:`. The existing
|
|
189
|
+
query-time budget continues to bound traversal work.
|
|
190
|
+
|
|
191
|
+
#### Users and roles can inspect effective object access
|
|
192
|
+
|
|
193
|
+
- **NEW**: `Parse::User` and `Parse::Role` expose `can_read?`, `can_write?`,
|
|
194
|
+
and `can_delete?` predicates backed by the new `Parse::Access.check` policy
|
|
195
|
+
preflight. Each check combines the target ACL with its class's `get`,
|
|
196
|
+
`update`, or `delete` CLP; delete uses the ACL write grant. Direct and
|
|
197
|
+
inherited user/role grants are supported, as are Parse Server's
|
|
198
|
+
`pointerFields`, `readUserFields`, and `writeUserFields` branch semantics.
|
|
199
|
+
`Parse::Access::Decision` and the instance `access_decision` /
|
|
200
|
+
`access_decisions` helpers expose `allowed`, `denied`, or `unknown` results;
|
|
201
|
+
the boolean predicates accept only a definite allow and otherwise fail
|
|
202
|
+
closed. Full rows with no ACL retain Parse Server's public default, while
|
|
203
|
+
pointers, partial rows, unresolved schema/role evidence, and unsupported
|
|
204
|
+
system-class rules remain unknown. `_User` reads and mutations honor Parse
|
|
205
|
+
Server's self-access rules, and role-only checks cannot claim a concrete
|
|
206
|
+
member's pointer or `_User` self permission. CLP cache entries are isolated
|
|
207
|
+
by Parse application so identically named classes cannot leak policy across
|
|
208
|
+
clients. These helpers are advisory—the eventual Parse Server request is
|
|
209
|
+
still authoritative.
|
|
210
|
+
|
|
211
|
+
#### Test infrastructure
|
|
212
|
+
|
|
213
|
+
- **CHANGED**: The integration test stack pins Parse Server 9.10.0, up from
|
|
214
|
+
9.9.0.
|
|
215
|
+
- **CHANGED**: The integration stack now backs Parse Server's own session,
|
|
216
|
+
user, and role caches with Redis instead of its in-process adapter, so the
|
|
217
|
+
`<appId>:role:<userId>` entries the upstream reader consumes are observable
|
|
218
|
+
from outside the container. It occupies database 1 while the SDK's cache and
|
|
219
|
+
create-locks stay on database 0, and the adapter refuses to start on database
|
|
220
|
+
0. Leaving the URL unset keeps the in-process adapter and the previous
|
|
221
|
+
behavior.
|
|
222
|
+
|
|
223
|
+
#### Session-token and role resolution move off Atlas Search and onto the client
|
|
224
|
+
|
|
225
|
+
- **NEW**: `Parse::Authorization` is the new owner of session-token resolution
|
|
226
|
+
and role-closure expansion. `client.authorization` returns a
|
|
227
|
+
`Parse::Authorization::Context`, one per `Parse::Client` and never shared.
|
|
228
|
+
Session-token resolution and role-closure expansion were originally written
|
|
229
|
+
inside `Parse::AtlasSearch::Session`, because `$search` was the first
|
|
230
|
+
feature to run aggregations straight against MongoDB and therefore the
|
|
231
|
+
first to enforce ACLs itself. Everything since reached back through it:
|
|
232
|
+
`Parse::ACLScope` called into the Atlas Search namespace, and
|
|
233
|
+
`Parse::MongoDB.aggregate` calls `Parse::ACLScope`, so
|
|
234
|
+
`Parse::Query#results_direct` on a plain query with no `$search` anywhere
|
|
235
|
+
in it depended on Atlas Search to decide who the caller was.
|
|
236
|
+
`Parse::Authorization` is now the sole owner; Atlas Search is one consumer
|
|
237
|
+
of it alongside every other mongo-direct path.
|
|
238
|
+
- **FIXED**: Two `Parse::Client` instances addressing two different Parse
|
|
239
|
+
applications no longer share one identity cache and one role cache. The
|
|
240
|
+
previous caches, TTLs, and resolver were module-level globals reachable
|
|
241
|
+
only through `Parse.client`, so a session token minted by a secondary
|
|
242
|
+
application's Parse Server could be validated against the default
|
|
243
|
+
application's `/users/me` call and its cached role closures. Each
|
|
244
|
+
`Parse::Authorization::Context` now holds a back-reference to the one
|
|
245
|
+
client it authorizes for and resolves exclusively through it.
|
|
246
|
+
- **NEW**: `Parse::Authorization.configure(identity_cache:, role_cache:,
|
|
247
|
+
identity_cache_ttl:, role_cache_ttl:, upstream_role_reader:,
|
|
248
|
+
compare_upstream_roles:)` configures the default client's context, as a
|
|
249
|
+
boundary convenience matching the existing single-application shorthand
|
|
250
|
+
`Parse::AtlasSearch.search(..., client: Parse.client)`. It is not the
|
|
251
|
+
source of truth: configure a secondary application with
|
|
252
|
+
`other_client.authorization.configure(...)` directly.
|
|
253
|
+
`Parse::Authorization.resolve(session_token, client:)` requires `client:`
|
|
254
|
+
with no default, because below the API boundary there is no such thing as
|
|
255
|
+
"the" client, and defaulting it there is exactly the bug this release
|
|
256
|
+
closes.
|
|
257
|
+
- **CHANGED**: The identity plane is renamed `identity_cache` (was
|
|
258
|
+
`session_cache`) and its TTL setting `identity_cache_ttl` (was
|
|
259
|
+
`session_cache_ttl`), because it stores one user id per session token and
|
|
260
|
+
never any `_Session` row or session object, and the old name led readers to
|
|
261
|
+
reason about `_Session` semantics that were never involved.
|
|
262
|
+
`Parse::Authorization::Resolved`, `::MemoryCache`, and `::InvalidSession`
|
|
263
|
+
replace `Parse::AtlasSearch::Session::Resolved`, `::MemoryCache`, and
|
|
264
|
+
`::InvalidSession`.
|
|
265
|
+
- **DEPRECATED**: `Parse::AtlasSearch.session_cache=`, `.role_cache=`,
|
|
266
|
+
`.session_cache_ttl`, `.role_cache_ttl`, `.upstream_role_reader`,
|
|
267
|
+
`.compare_upstream_roles`, and `Parse::AtlasSearch::Session.resolve` /
|
|
268
|
+
`.invalidate` / `.invalidate_user_roles` / `.reset_caches!` all still work
|
|
269
|
+
and delegate to the default client's context, so existing code keeps
|
|
270
|
+
running unchanged. Being module-level, they can only ever address
|
|
271
|
+
`Parse.client`; code running against a secondary application must call
|
|
272
|
+
`other_client.authorization` directly. Slated for removal in 6.0.
|
|
273
|
+
`Parse::AtlasSearch.require_session_token` is not part of this move: it
|
|
274
|
+
decides whether `$search` may run anonymously, which is Atlas Search's own
|
|
275
|
+
policy, not an identity concern.
|
|
276
|
+
|
|
277
|
+
#### Cache clears can no longer widen past what was asked
|
|
278
|
+
|
|
279
|
+
- **FIXED**: `Parse::Cache::Redis#clear` accepted `family:` and `tenant:` and
|
|
280
|
+
silently ignored them, falling through to the unnamespaced branch and
|
|
281
|
+
issuing `FLUSHDB`. A request to clear one family therefore wiped the whole
|
|
282
|
+
database, including other applications' entries and the
|
|
283
|
+
`parse-stack:foc:v1:*` create-locks, whose loss silently removes
|
|
284
|
+
`first_or_create!` mutual exclusion. `clear` now raises `ArgumentError` for
|
|
285
|
+
that combination and points callers at
|
|
286
|
+
`backend.scoped(keyspace).clear(family:)`, so a request to narrow a clear
|
|
287
|
+
can no longer widen it.
|
|
288
|
+
- **FIXED**: `cache_keyspace: true` on a store that cannot produce a scoped
|
|
289
|
+
view, such as a plain `Moneta.new(:Redis)`, left the store installed bare.
|
|
290
|
+
Key composition still worked, so the deployment looked correctly
|
|
291
|
+
keyspaced, but `Parse::Client#clear_cache!` called the store's own
|
|
292
|
+
unrestricted `clear`, which on Redis is `FLUSHDB`. Such stores are now
|
|
293
|
+
wrapped in `Parse::Cache::KeyspacedStore`, which clears by enumerating
|
|
294
|
+
keys under the keyspace where the store supports `each_key`, and raises
|
|
295
|
+
`Parse::Cache::UnscopedClearRefused` where it cannot, rather than widening
|
|
296
|
+
the clear to compensate.
|
|
297
|
+
|
|
298
|
+
#### The upstream-isolation probe stops mistaking silence for isolation
|
|
299
|
+
|
|
300
|
+
- **FIXED**: `Parse::Cache::Redis#verify_upstream_isolation!` reported an
|
|
301
|
+
empty shared database as isolated. The SCAN probe can only ever prove
|
|
302
|
+
sharing: an empty result is equally consistent with a genuinely separate
|
|
303
|
+
database and with a shared one on which Parse Server has not yet cached a
|
|
304
|
+
role closure, which is the state of every freshly deployed stack and
|
|
305
|
+
exactly when an operator runs the check. The method now falls back to
|
|
306
|
+
writing a random sentinel into the SDK's own database and asking the
|
|
307
|
+
upstream connection to read it back, and returns `true` for established
|
|
308
|
+
isolation, `false` for established sharing, or `:unknown` when neither
|
|
309
|
+
could be shown, which is what a credential restricted to
|
|
310
|
+
`~<appId>:role:*` produces since the sentinel read is denied and a denial
|
|
311
|
+
says nothing about which database denied it. `:unknown` is truthy, so
|
|
312
|
+
callers branching on truthiness are unaffected.
|
|
313
|
+
|
|
314
|
+
#### Generation keys stop growing without bound
|
|
315
|
+
|
|
316
|
+
- **FIXED**: Generation keys in `Parse::Cache::SubCache` never expired. One
|
|
317
|
+
key is written per user id on every `_User` webhook, which is unbounded
|
|
318
|
+
Redis growth on a public signup flow. Generation keys now expire at twice
|
|
319
|
+
the plane's entry TTL: expiry resets a counter to 0, which is also the
|
|
320
|
+
value for a subject never bumped, so a counter that outlived its entries
|
|
321
|
+
would let an entry written at generation 0 compare current again and
|
|
322
|
+
reappear after having been invalidated. `set` clamps any longer per-call
|
|
323
|
+
TTL to keep that invariant true.
|
|
324
|
+
|
|
325
|
+
### Behavior Notes
|
|
326
|
+
|
|
327
|
+
- Authorization is client-owned as of this release: `client.authorization`
|
|
328
|
+
owns session-token resolution and role-closure expansion for that client
|
|
329
|
+
alone. `Parse::MongoDB` (the URI, the driver connection, and collection
|
|
330
|
+
selection) stays process-global in this release; the Mongo connection
|
|
331
|
+
itself becomes client-owned in 6.0.
|
|
332
|
+
- **NEW**: Because those two now have different owners, `Parse::MongoDB`
|
|
333
|
+
records the Parse application it was configured for and
|
|
334
|
+
`Parse::MongoDB.verify_client!` refuses a mongo-direct query authorized by
|
|
335
|
+
a client belonging to a different one, raising
|
|
336
|
+
`Parse::MongoDB::ClientMismatch`. Per-client authorization and a
|
|
337
|
+
process-global connection are each safe alone and dangerous together: a
|
|
338
|
+
secondary client would resolve its token correctly against its own
|
|
339
|
+
application, build a correct `_rperm` allow-set for one of its users, then
|
|
340
|
+
run the pipeline against the other application's database, where those user
|
|
341
|
+
ids and role names match rows they have nothing to do with. Nothing about
|
|
342
|
+
that looks like a failure, which is why it fails closed instead. A
|
|
343
|
+
connection with no recorded binding, and a caller that cannot be
|
|
344
|
+
identified, both proceed, so single-application deployments and master-mode
|
|
345
|
+
calls made before `Parse.setup` are unaffected. The guard becomes
|
|
346
|
+
unnecessary in 6.0.
|
|
347
|
+
- **NEW**: `Parse::Query#results_direct`, `#count_direct`, `#distinct_direct`,
|
|
348
|
+
`#distinct_direct_pointers`, and `Parse::MongoDB.aggregate` accept `client:`
|
|
349
|
+
alongside the existing auth keywords. It names the authorization context
|
|
350
|
+
that resolves the call, and it is carried onto the `Parse::ACLScope`
|
|
351
|
+
resolution so the binding check above has something to compare. Without it
|
|
352
|
+
every direct read resolved through `Parse.client`, which left the check
|
|
353
|
+
unable to see a second client at all and therefore unable to catch the case
|
|
354
|
+
it was written for. Omitting the keyword resolves through `Parse.client` as
|
|
355
|
+
before.
|
|
356
|
+
- `cache_keyspace: true` is the switch for the new cache layout, scoped
|
|
357
|
+
clearing, invalidation hooks, and shared identity and role planes. Left
|
|
358
|
+
unset, those cache behaviors are exactly as they were. `parse_cache_url:` is
|
|
359
|
+
separately opt-in, and without it no upstream endpoint is contacted.
|
|
360
|
+
- `parse_cache_url:` must address a different Redis database from `url:`. The
|
|
361
|
+
two are read and written by different processes with different clearing
|
|
362
|
+
semantics, and until the scoped-clear fix lands upstream a single `_Role`
|
|
363
|
+
write on the shared database destroys the SDK's cache and its create-locks.
|
|
364
|
+
- Create-locks deliberately keep their historical `parse-stack:foc:v1:` prefix
|
|
365
|
+
and are not relocated into the keyspace. Moving them would have two workers
|
|
366
|
+
compute different lock keys during a rolling deploy, so they would stop
|
|
367
|
+
contending on the same key and lose mutual exclusion for the length of the
|
|
368
|
+
deploy.
|
|
369
|
+
- The built-in upstream-role integration is compare-only and never changes
|
|
370
|
+
`permission_strings` or an ACL decision. A caller that directly consumes
|
|
371
|
+
`roles_for` as authorization input makes that database part of its trust
|
|
372
|
+
base, so restrict the credential to `+get +pttl` on `<appId>:role:*`.
|
|
373
|
+
- Webhook-driven invalidation requires the application to expose a webhook
|
|
374
|
+
endpoint Parse Server can reach and to have registered the hooks. Where it is
|
|
375
|
+
unregistered or unreachable, the TTL is the only bound on staleness.
|
|
376
|
+
|
|
377
|
+
### Code Example
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
# The SDK's own cache on database 0, Parse Server's cache read-only on 1.
|
|
381
|
+
store = Parse::Cache::Redis.new(
|
|
382
|
+
url: "redis://localhost:6379/0",
|
|
383
|
+
parse_cache_url: "redis://localhost:6379/1",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
Parse.setup(
|
|
387
|
+
server_url: ENV.fetch("PARSE_SERVER_URL"),
|
|
388
|
+
application_id: ENV.fetch("PARSE_APP_ID"),
|
|
389
|
+
master_key: ENV.fetch("PARSE_MASTER_KEY"),
|
|
390
|
+
cache: store,
|
|
391
|
+
expires: 10,
|
|
392
|
+
cache_keyspace: true, # reserved keyspace, scoped clearing, hook install
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Warns when both URLs resolve to the same Redis database.
|
|
396
|
+
store.verify_upstream_isolation!
|
|
397
|
+
|
|
398
|
+
# Share identity and role resolution across every process. Each client owns
|
|
399
|
+
# its own Parse::Authorization::Context, so a second client pointed at a
|
|
400
|
+
# second application configures its own view the same way.
|
|
401
|
+
view = Parse.client.sdk_cache # the scoped view derived at setup
|
|
402
|
+
Parse::Authorization.configure(
|
|
403
|
+
identity_cache: view.identity(ttl: 3600),
|
|
404
|
+
role_cache: view.roles(ttl: 30),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
Parse.client.clear_cache! # scoped SCAN, because a keyspace is configured
|
|
408
|
+
view.clear(family: :role) # one plane
|
|
409
|
+
view.clear(family: :cache, tenant: "acme")
|
|
410
|
+
```
|
|
411
|
+
|
|
3
412
|
### 5.6.0
|
|
4
413
|
|
|
5
414
|
#### Voyage embeddings reach the Atlas endpoint, video, and streamed media
|