parse-stack-next 5.7.0 → 5.7.2
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 +167 -1
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/cache/invalidation.rb +102 -50
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +25 -4
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- 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/core/actions.rb +294 -123
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/fetching.rb +4 -0
- data/lib/parse/model/core/properties.rb +4 -0
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +4 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +79 -5
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/webhooks.rb +98 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b773e88a55f8771a7d898978652e9be673ec496627fcbaa0b0eb68b950486812
|
|
4
|
+
data.tar.gz: 7813d865cb6cc09b6c44070186dbf67383304d9407a5de58fdbb723cce4dc081
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ffa5266044c369c673c8c5e4e57d4b37d882674eae6d8f7c79f3b1a8318a75dbd76d97036337d8a5416cfdcc022c1db3353e20de53857eaf8e37ac29913dfa7
|
|
7
|
+
data.tar.gz: e897bb63c7030751cbb2a9720e6fde91e23f6c118a6656e9063a84df4ec7305db1e3f8dd141e600f2e9765fb3660fdd26b0c36c2c321184c9743c6be976498c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,171 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.7.2
|
|
4
|
+
|
|
5
|
+
#### `between` accepts Ruby Range values
|
|
6
|
+
|
|
7
|
+
- **NEW**: The `between` constraint now accepts a Ruby `Range` in addition to
|
|
8
|
+
a 2-element array, so `Person.where(:age.between => 5..25)` and
|
|
9
|
+
`Record.where(:date.between => 5.days.ago...2.days.ago)` work directly. An
|
|
10
|
+
inclusive range (`..`) maps its upper bound to `$lte`, matching the existing
|
|
11
|
+
array form, while an exclusive range (`...`) maps it to `$lt` instead.
|
|
12
|
+
Beginless (`..25`) and endless (`5..`) ranges are also supported and
|
|
13
|
+
constrain only the side that is present, so `Person.where(:age.between =>
|
|
14
|
+
18..)` compiles to `{"$gte" => 18}` with no upper bound. The array form is
|
|
15
|
+
unchanged, and both forms produce identical output for the same bounds.
|
|
16
|
+
|
|
17
|
+
#### `Query#where_not_between` for the negated form of a range
|
|
18
|
+
|
|
19
|
+
- **NEW**: `Query#where_not_between(field, value)` adds the logical negation
|
|
20
|
+
of `between`: `Person.query.where_not_between(:age, 5..25)` compiles to
|
|
21
|
+
`age < 5 OR age > 25`, accepting the same Range and 2-element Array forms
|
|
22
|
+
as `between` (exclusive ranges flip the upper side to `$gte`, and a
|
|
23
|
+
beginless or endless Range negates to a single one-sided comparison with
|
|
24
|
+
no `$or` needed). It is not available as a `field.not_between => value`
|
|
25
|
+
symbol constraint: a range's negation is inherently an `$or` of two
|
|
26
|
+
comparisons, and only one `$or` group can be safely merged into a
|
|
27
|
+
compiled query, so a symbol constraint that unilaterally emitted one
|
|
28
|
+
could silently collide with an existing `$or` from `or_where`/`|`.
|
|
29
|
+
`where_not_between` instead composes the negation the way
|
|
30
|
+
`Parse::Query.and` already does, so it correctly nests inside a query's
|
|
31
|
+
other `.where` conditions instead of replacing them, and raises
|
|
32
|
+
`ArgumentError` if the query already has an `$or` group rather than
|
|
33
|
+
silently dropping part of it.
|
|
34
|
+
|
|
35
|
+
#### `embed_image` forwards a presigned URL when the source file has one
|
|
36
|
+
|
|
37
|
+
- **FIXED**: `embed_image` always sent the source file's bare `file.url` to
|
|
38
|
+
the embedding provider (or to the SDK's own `:bytes`-mode downloader). On a
|
|
39
|
+
private-bucket file adapter (S3/GCS configured with `presignedUrl: true`),
|
|
40
|
+
`file.url` is the canonical URL with its signature stripped, so the
|
|
41
|
+
provider's fetch (or the SDK's download) got a 403 instead of the image.
|
|
42
|
+
`Parse::File` already captures the signed variant in `file.presigned_url`
|
|
43
|
+
whenever Parse Server returns one, but `embed_image` never read it.
|
|
44
|
+
Recompute now forwards `file.presigned_url` when it is present and not yet
|
|
45
|
+
expired, and falls back to the bare URL otherwise, for both `source: :url`
|
|
46
|
+
and `source: :bytes`. The stored digest is still keyed on the bare
|
|
47
|
+
canonical URL, so a save that only rotates the file's signature does not
|
|
48
|
+
trigger a needless re-embed. The validity check ignores
|
|
49
|
+
`presigned_url_valid?`'s default 60-second safety buffer (meant for a
|
|
50
|
+
browser render, not an immediate server-side fetch), since on a
|
|
51
|
+
private-bucket adapter the fallback URL is not fetchable at all and would
|
|
52
|
+
otherwise 403 for the last minute of every signature's life.
|
|
53
|
+
`Parse::Embeddings::ImageFetch::FetchedImage#url` now stores the
|
|
54
|
+
query-stripped URL rather than the presigned one, since a live signature
|
|
55
|
+
has no reason to survive into that value object's `#inspect` output.
|
|
56
|
+
`source: :url` mode can now forward a presigned URL to the embedding
|
|
57
|
+
provider under the same `Parse::Embeddings.trust_provider_url_fetch`
|
|
58
|
+
consent already required to forward any URL; operators relying on private
|
|
59
|
+
buckets should confirm the provider's egress handling covers
|
|
60
|
+
credential-bearing URLs, not just public ones.
|
|
61
|
+
|
|
62
|
+
#### `Query#get` now resolves aliased `parse_class` names correctly
|
|
63
|
+
|
|
64
|
+
- **FIXED**: `Query#get` looked up the target class with a raw
|
|
65
|
+
`Object.const_get(@table)`, which only worked when the Parse class name
|
|
66
|
+
matched the Ruby constant name exactly. A model that renames its table via
|
|
67
|
+
`parse_class "SomeOtherName"` was never found by this lookup, so `get`
|
|
68
|
+
silently fell back to a generic `Parse::Object`/`Parse::Pointer` instead of
|
|
69
|
+
hydrating the declared model. `Query#get` now passes the table name through
|
|
70
|
+
to `Parse::Object.build` as a string, letting it run its own
|
|
71
|
+
`Parse::Model.find_class` resolution, which already understands
|
|
72
|
+
`parse_class` aliasing.
|
|
73
|
+
|
|
74
|
+
#### `_safe_warn` now writes through a configured logger
|
|
75
|
+
|
|
76
|
+
- **FIXED**: Internal warnings for authentication, timeout, and cloud-code
|
|
77
|
+
errors (`Parse::Client._safe_warn`) always wrote to STDERR, even when an
|
|
78
|
+
app had configured `Parse.logger = Rails.logger` (or any other logger) for
|
|
79
|
+
the rest of its Parse request/response logging. These warnings now route
|
|
80
|
+
through `Parse::Middleware::Logging.logger` when one is configured, so they
|
|
81
|
+
land in the same place as the app's other logs; STDERR remains the fallback
|
|
82
|
+
when no logger is configured, matching prior behavior. Every call site
|
|
83
|
+
raises the corresponding typed `Parse::Error` immediately after this
|
|
84
|
+
warning, so a configured logger that itself raises (a closed handle, a
|
|
85
|
+
full disk, a remote-aggregator client erroring on a socket) now falls back
|
|
86
|
+
to STDERR rather than propagating in place of the real error and masking
|
|
87
|
+
it.
|
|
88
|
+
|
|
89
|
+
### 5.7.1
|
|
90
|
+
|
|
91
|
+
#### Cache-invalidation webhooks no longer break every application hook for the same trigger
|
|
92
|
+
|
|
93
|
+
- **FIXED**: `Parse::Cache::Invalidation` raised `NoMethodError: undefined
|
|
94
|
+
method 'guard'` on every `_User`, `_Role`, and `_Session` trigger it
|
|
95
|
+
registered, and the failure was not contained. A webhook handler block is
|
|
96
|
+
bound to the payload before it runs, so the bare `guard` / `bump_subject` /
|
|
97
|
+
`subject_id` calls in the handler bodies resolved against
|
|
98
|
+
`Parse::Webhooks::Payload`, which defines none of them. `guard`'s own
|
|
99
|
+
`rescue` never ran, because it lives inside the body that was never entered,
|
|
100
|
+
so the error escaped into the route dispatcher. The dispatcher folds a
|
|
101
|
+
trigger's handlers with `Array#map`, which abandons the collection on the
|
|
102
|
+
first raise, and these triggers install during `Parse.setup` and therefore
|
|
103
|
+
sit ahead of every application handler for the same trigger. One
|
|
104
|
+
unresolvable method name silently prevented an application's own
|
|
105
|
+
`after_save "_User"` hook from running at all, so any work downstream of it
|
|
106
|
+
stopped without a visible cause. The handlers now call the module on an
|
|
107
|
+
explicit receiver captured in a local, which is independent of whatever
|
|
108
|
+
`self` the dispatcher binds. Applications that set
|
|
109
|
+
`cache_invalidation_hooks: false` to work around this can remove it.
|
|
110
|
+
- **FIXED**: The invalidation tests dispatched handlers with a plain
|
|
111
|
+
`Proc#call`, which leaves `self` bound to the module the block closed over,
|
|
112
|
+
so every one of them passed against handlers that could not run in
|
|
113
|
+
production. They now dispatch through the real handler invocation and fail
|
|
114
|
+
against the broken code.
|
|
115
|
+
|
|
116
|
+
#### Whether one failing `after_*` handler stops the rest is now a setting
|
|
117
|
+
|
|
118
|
+
- **NEW**: `Parse::Webhooks.abort_after_callbacks_on_error` decides whether an
|
|
119
|
+
exception raised by one `after_*` handler prevents the remaining handlers for
|
|
120
|
+
that trigger from running. It defaults to `true`, which is the existing
|
|
121
|
+
behavior, so nothing changes on upgrade. Set it to `false` to isolate
|
|
122
|
+
handlers from each other: each one runs regardless of what an earlier one
|
|
123
|
+
raised, and the failure is reported with a warning and a
|
|
124
|
+
`parse.webhooks.handler_error` notification instead of being swallowed
|
|
125
|
+
silently. This was previously not a decision at all but a side effect of
|
|
126
|
+
folding handlers with `Array#map`, which abandons the collection on the
|
|
127
|
+
first raise. Registration order is not fully under an application's control,
|
|
128
|
+
since the SDK's own cache-invalidation triggers install during `Parse.setup`
|
|
129
|
+
and therefore sit ahead of handlers registered by application files loaded
|
|
130
|
+
later, so a single raising handler could silently prevent an application's
|
|
131
|
+
own hooks from running with nothing logged to say they had been skipped.
|
|
132
|
+
- **BEHAVIOR**: The setting governs only whether later handlers still run.
|
|
133
|
+
Neither mode reverts anything: an `after_*` trigger fires once the write has
|
|
134
|
+
already committed, so there is no version of this setting that can undo a
|
|
135
|
+
save. `before_*` dispatch is untouched, and only the accumulating,
|
|
136
|
+
non-rejectable triggers (`after_save`, `after_delete`, `after_logout`) can
|
|
137
|
+
hold more than one handler in the first place. A rejectable `before_*`
|
|
138
|
+
trigger must deny if any handler denies, so its raise must continue to
|
|
139
|
+
abort.
|
|
140
|
+
|
|
141
|
+
#### Failed transactions restore the object they rolled back
|
|
142
|
+
|
|
143
|
+
- **FIXED**: A failed `Parse::Object.transaction` left the in-memory object
|
|
144
|
+
holding its modified values. The rollback snapshotted `Parse::Object#attributes`
|
|
145
|
+
and restored it, but that method returns a schema map of field name to type
|
|
146
|
+
symbol rather than values, so nothing was ever restored. Property values
|
|
147
|
+
live in `@<field>` instance variables; those are now what the rollback
|
|
148
|
+
captures and restores, including values nested inside arrays and hashes,
|
|
149
|
+
relation operation queues, and properties whose ivar did not exist before
|
|
150
|
+
the transaction. State is captured when the object first enters the
|
|
151
|
+
transaction rather than at `batch.add`, so the documented pattern of
|
|
152
|
+
mutating an object and adding it afterwards rolls back correctly.
|
|
153
|
+
- **FIXED**: A failed transaction also left the object with broken change
|
|
154
|
+
tracking. Restoring the schema map defined `@attributes`, which is the
|
|
155
|
+
instance variable `ActiveModel::Dirty` keys its behavior on: once defined it
|
|
156
|
+
builds an `AttributeMutationTracker` over that hash instead of the
|
|
157
|
+
`ForcedMutationTracker` a `Parse::Object` needs, and `clear_changes!` then
|
|
158
|
+
called `forgetting_assignment` on the `[key, value]` pairs `Hash#map`
|
|
159
|
+
yields. Both call sites rescue and warn, so every rollback quietly
|
|
160
|
+
downgraded the object rather than failing. The rollback no longer defines
|
|
161
|
+
`@attributes`.
|
|
162
|
+
- **FIXED**: The mongo-direct role-graph integration test gated on
|
|
163
|
+
`ANALYTICS_DATABASE_URI`, a production variable name the test stack never
|
|
164
|
+
sets, so both of its traversal assertions skipped on every run while the
|
|
165
|
+
per-file reporter still reported the file as passing. It now reads
|
|
166
|
+
`PARSE_TEST_MONGO_URI` like every other mongo-direct integration file and
|
|
167
|
+
still honors `ANALYTICS_DATABASE_URI` as an override.
|
|
168
|
+
|
|
3
169
|
### 5.7.0
|
|
4
170
|
|
|
5
171
|
#### Cache keys move into a reserved, app-scoped keyspace
|
|
@@ -205,7 +371,7 @@
|
|
|
205
371
|
Server's self-access rules, and role-only checks cannot claim a concrete
|
|
206
372
|
member's pointer or `_User` self permission. CLP cache entries are isolated
|
|
207
373
|
by Parse application so identically named classes cannot leak policy across
|
|
208
|
-
clients. These helpers are advisory
|
|
374
|
+
clients. These helpers are advisory: the eventual Parse Server request is
|
|
209
375
|
still authoritative.
|
|
210
376
|
|
|
211
377
|
#### Test infrastructure
|
|
@@ -191,7 +191,7 @@ module Parse
|
|
|
191
191
|
|
|
192
192
|
result_hash = dispatch(method, params, agent, id, logger, subscription_manager)
|
|
193
193
|
{ status: result_hash[:status], body: result_hash[:body] }
|
|
194
|
-
rescue Parse::Agent::Unauthorized
|
|
194
|
+
rescue Parse::Agent::Unauthorized
|
|
195
195
|
{ status: 401, body: jsonrpc_error(body.is_a?(Hash) ? body["id"] : nil, -32001, "Unauthorized") }
|
|
196
196
|
rescue StandardError => e
|
|
197
197
|
# Do not leak the exception class name (gem fingerprinting). Server-
|
|
@@ -295,7 +295,7 @@ module Parse
|
|
|
295
295
|
else
|
|
296
296
|
{ status: 200, body: jsonrpc_envelope(id, result: result) }
|
|
297
297
|
end
|
|
298
|
-
rescue Parse::Agent::Unauthorized
|
|
298
|
+
rescue Parse::Agent::Unauthorized
|
|
299
299
|
{ status: 401, body: jsonrpc_error(id, -32001, "Unauthorized") }
|
|
300
300
|
rescue Parse::Agent::AccessDenied
|
|
301
301
|
# Class-authorization denial (agent_hidden / classes: allowlist), e.g.
|
data/lib/parse/agent.rb
CHANGED
|
@@ -2475,7 +2475,6 @@ module Parse
|
|
|
2475
2475
|
end
|
|
2476
2476
|
|
|
2477
2477
|
ActiveSupport::Notifications.instrument("parse.agent.tool_call", payload) do
|
|
2478
|
-
response = nil
|
|
2479
2478
|
# Install a fresh embedding accumulator for this tool span. The
|
|
2480
2479
|
# process-wide "parse.embeddings.embed" subscriber records each
|
|
2481
2480
|
# embed into it; the ensure below reads + restores it so the
|
|
@@ -52,26 +52,111 @@ module Parse
|
|
|
52
52
|
registered
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Run a role-trigger invalidation.
|
|
56
|
+
#
|
|
57
|
+
# Public because the registered webhook block calls it on an explicit
|
|
58
|
+
# receiver. See {handle_identity_trigger} for why that matters.
|
|
59
|
+
#
|
|
60
|
+
# @!visibility private
|
|
61
|
+
# @param cache [Parse::Cache::Redis] the keyspace-configured cache.
|
|
62
|
+
# @return [void]
|
|
63
|
+
def handle_role_trigger(cache)
|
|
64
|
+
guard do
|
|
65
|
+
# A role write does not say which users are affected: membership
|
|
66
|
+
# and hierarchy changes arrive as relation deltas on `users` and
|
|
67
|
+
# `roles`, and the cached value is a flattened transitive closure,
|
|
68
|
+
# so a parent-role change reaches the members of every child.
|
|
69
|
+
# Clearing the whole plane is both correct and cheap under a
|
|
70
|
+
# scoped SCAN. Parse Server does the same, for the same reason.
|
|
71
|
+
cache.roles.clear
|
|
72
|
+
# Stamp the epoch so a *foreign* role entry written before this
|
|
73
|
+
# moment is rejected on read. Parse Server does not clear its own
|
|
74
|
+
# role cache on a `_Role` delete, so without this the next read
|
|
75
|
+
# would take its stale entry back and our clear would shorten
|
|
76
|
+
# revocation by nothing.
|
|
77
|
+
cache.roles.touch_epoch
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Run an identity-trigger invalidation.
|
|
82
|
+
#
|
|
83
|
+
# Public, and invoked on an explicit receiver, because a webhook
|
|
84
|
+
# handler block does NOT run with `self` bound to the module that
|
|
85
|
+
# created it. {Parse::Webhooks.invoke_handler} binds the block to the
|
|
86
|
+
# payload (`payload.define_singleton_method(name, &block)`), and the
|
|
87
|
+
# historical `payload.instance_exec(payload, &block)` did the same.
|
|
88
|
+
# A block body calling a bare `guard` / `bump_subject` / `subject_id`
|
|
89
|
+
# therefore resolved against {Parse::Webhooks::Payload}, which defines
|
|
90
|
+
# none of them, and raised `NoMethodError` on every `_User`, `_Role`,
|
|
91
|
+
# and `_Session` trigger.
|
|
92
|
+
#
|
|
93
|
+
# That failure was not contained. `guard`'s `rescue` lives inside
|
|
94
|
+
# `guard`'s own body, which was never entered, so the error escaped
|
|
95
|
+
# into `call_route`'s `registry.map`. `Array#map` abandons the whole
|
|
96
|
+
# collection on the first raise, and these triggers register at
|
|
97
|
+
# `Parse.setup` and therefore sit AHEAD of any application handler for
|
|
98
|
+
# the same trigger. One unresolvable method name silently prevented
|
|
99
|
+
# every application `after_save "_User"` hook from running at all.
|
|
100
|
+
#
|
|
101
|
+
# Capturing the module in a local and calling it explicitly is what
|
|
102
|
+
# makes the handler independent of whatever `self` the dispatcher
|
|
103
|
+
# binds.
|
|
104
|
+
#
|
|
105
|
+
# @!visibility private
|
|
106
|
+
# @param cache [Parse::Cache::Redis] the keyspace-configured cache.
|
|
107
|
+
# @param type [Symbol] the trigger being handled.
|
|
108
|
+
# @param payload [Parse::Webhooks::Payload] the incoming payload.
|
|
109
|
+
# @return [void]
|
|
110
|
+
def handle_identity_trigger(cache, type, payload)
|
|
111
|
+
guard do
|
|
112
|
+
case type
|
|
113
|
+
when :after_logout
|
|
114
|
+
# The only trigger Parse Server permits on `_Session`. The
|
|
115
|
+
# object's own sessionToken is scrubbed from the payload, but
|
|
116
|
+
# the token is captured from the requesting user before
|
|
117
|
+
# scrubbing, and for a logout that user *is* the session being
|
|
118
|
+
# ended. A master-key logout carries no user, so fall back to
|
|
119
|
+
# the generation bump.
|
|
120
|
+
#
|
|
121
|
+
# Pass the RAW token, not a pre-hashed digest.
|
|
122
|
+
# `Parse::Cache::SubCache#invalidate` hashes its `key`
|
|
123
|
+
# argument internally for the `:idn` family (see
|
|
124
|
+
# `SubCache#logical_key`), the same way `#get` / `#set` do.
|
|
125
|
+
# That is what makes a `set(raw_token, ...)` /
|
|
126
|
+
# `get(raw_token)` pair round-trip. Hashing here first and
|
|
127
|
+
# handing SubCache an already-hashed value made it hash the
|
|
128
|
+
# digest a second time, landing on a key nothing had ever
|
|
129
|
+
# written to, so logout silently failed to evict the entry.
|
|
130
|
+
token = payload.respond_to?(:session_token) ? payload.session_token : nil
|
|
131
|
+
if token && !token.to_s.empty?
|
|
132
|
+
cache.identity.invalidate(token.to_s)
|
|
133
|
+
else
|
|
134
|
+
bump_subject(cache, subject_id(payload))
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
# A `_User` write gives a user id, but identity entries are
|
|
138
|
+
# keyed by session token and no reverse map exists. Bumping a
|
|
139
|
+
# per-user generation invalidates every one of that user's
|
|
140
|
+
# entries in O(1), including tokens this process has never
|
|
141
|
+
# resolved, and without Parse Server's master-key `_Session`
|
|
142
|
+
# query.
|
|
143
|
+
bump_subject(cache, subject_id(payload))
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
55
148
|
private
|
|
56
149
|
|
|
57
150
|
def install_role_triggers!(cache)
|
|
151
|
+
# `invalidator` is a captured LOCAL, not `self`. The registered block
|
|
152
|
+
# runs with `self` rebound to the payload, so a bare method call in
|
|
153
|
+
# its body would resolve against `Parse::Webhooks::Payload`. A local
|
|
154
|
+
# closes over correctly regardless of the receiver the dispatcher
|
|
155
|
+
# binds.
|
|
156
|
+
invalidator = self
|
|
58
157
|
TRIGGERS[:role].map do |(type, class_name)|
|
|
59
158
|
Parse::Webhooks.route(type, class_name) do |payload|
|
|
60
|
-
|
|
61
|
-
# A role write does not say which users are affected: membership
|
|
62
|
-
# and hierarchy changes arrive as relation deltas on `users` and
|
|
63
|
-
# `roles`, and the cached value is a flattened transitive closure,
|
|
64
|
-
# so a parent-role change reaches the members of every child.
|
|
65
|
-
# Clearing the whole plane is both correct and cheap under a
|
|
66
|
-
# scoped SCAN. Parse Server does the same, for the same reason.
|
|
67
|
-
cache.roles.clear
|
|
68
|
-
# Stamp the epoch so a *foreign* role entry written before this
|
|
69
|
-
# moment is rejected on read. Parse Server does not clear its own
|
|
70
|
-
# role cache on a `_Role` delete, so without this the next read
|
|
71
|
-
# would take its stale entry back and our clear would shorten
|
|
72
|
-
# revocation by nothing.
|
|
73
|
-
cache.roles.touch_epoch
|
|
74
|
-
end
|
|
159
|
+
invalidator.handle_role_trigger(cache)
|
|
75
160
|
true
|
|
76
161
|
end
|
|
77
162
|
[type, class_name]
|
|
@@ -79,43 +164,10 @@ module Parse
|
|
|
79
164
|
end
|
|
80
165
|
|
|
81
166
|
def install_identity_triggers!(cache)
|
|
167
|
+
invalidator = self
|
|
82
168
|
TRIGGERS[:identity].map do |(type, class_name)|
|
|
83
169
|
Parse::Webhooks.route(type, class_name) do |payload|
|
|
84
|
-
|
|
85
|
-
case type
|
|
86
|
-
when :after_logout
|
|
87
|
-
# The only trigger Parse Server permits on `_Session`. The
|
|
88
|
-
# object's own sessionToken is scrubbed from the payload, but
|
|
89
|
-
# the token is captured from the requesting user before
|
|
90
|
-
# scrubbing, and for a logout that user *is* the session being
|
|
91
|
-
# ended. A master-key logout carries no user, so fall back to
|
|
92
|
-
# the generation bump.
|
|
93
|
-
#
|
|
94
|
-
# Pass the RAW token, not a pre-hashed digest.
|
|
95
|
-
# `Parse::Cache::SubCache#invalidate` hashes its `key`
|
|
96
|
-
# argument internally for the `:idn` family (see
|
|
97
|
-
# `SubCache#logical_key`), the same way `#get` / `#set` do —
|
|
98
|
-
# that is what makes a `set(raw_token, ...)` /
|
|
99
|
-
# `get(raw_token)` pair round-trip. Hashing here first and
|
|
100
|
-
# handing SubCache an already-hashed value made it hash the
|
|
101
|
-
# digest a second time, landing on a key nothing had ever
|
|
102
|
-
# written to, so logout silently failed to evict the entry.
|
|
103
|
-
token = payload.respond_to?(:session_token) ? payload.session_token : nil
|
|
104
|
-
if token && !token.to_s.empty?
|
|
105
|
-
cache.identity.invalidate(token.to_s)
|
|
106
|
-
else
|
|
107
|
-
bump_subject(cache, subject_id(payload))
|
|
108
|
-
end
|
|
109
|
-
else
|
|
110
|
-
# A `_User` write gives a user id, but identity entries are
|
|
111
|
-
# keyed by session token and no reverse map exists. Bumping a
|
|
112
|
-
# per-user generation invalidates every one of that user's
|
|
113
|
-
# entries in O(1), including tokens this process has never
|
|
114
|
-
# resolved, and without Parse Server's master-key `_Session`
|
|
115
|
-
# query.
|
|
116
|
-
bump_subject(cache, subject_id(payload))
|
|
117
|
-
end
|
|
118
|
-
end
|
|
170
|
+
invalidator.handle_identity_trigger(cache, type, payload)
|
|
119
171
|
true
|
|
120
172
|
end
|
|
121
173
|
[type, class_name]
|
data/lib/parse/client/request.rb
CHANGED
|
@@ -17,12 +17,24 @@ module Parse
|
|
|
17
17
|
# @!attribute [rw] body
|
|
18
18
|
# @return [Hash] the body of this request.
|
|
19
19
|
|
|
20
|
-
# TODO: Document opts and cache options.
|
|
21
|
-
|
|
22
20
|
# @!attribute [rw] opts
|
|
23
|
-
# @return [Hash]
|
|
21
|
+
# @return [Hash] per-request options consumed by {Parse::Client#request}
|
|
22
|
+
# when it builds the HTTP headers for this request. Recognized keys:
|
|
23
|
+
# * `:cache` — `false` sends `Cache-Control: no-cache`; `:write_only`
|
|
24
|
+
# skips the cache read but still writes the response; a `Numeric`
|
|
25
|
+
# overrides the cache expiration (seconds) for this request only.
|
|
26
|
+
# * `:use_master_key` — `false` forces the master key off for this
|
|
27
|
+
# request even if the client has one configured.
|
|
28
|
+
# * `:session_token` — a session token to authenticate this request as
|
|
29
|
+
# a specific user, bypassing the client's default auth context.
|
|
30
|
+
# * `:idempotent` — explicitly enables/disables idempotency-header
|
|
31
|
+
# generation for this request, overriding the class-level defaults.
|
|
32
|
+
# * `:request_id` — a caller-supplied idempotency key; see
|
|
33
|
+
# {.enable_idempotency!}.
|
|
24
34
|
# @!attribute [rw] cache
|
|
25
|
-
# @return [Boolean]
|
|
35
|
+
# @return [Boolean] unused by {Parse::Request} itself; retained as a
|
|
36
|
+
# plain accessor for callers that stash a cache handle or flag
|
|
37
|
+
# directly on the request object rather than through `opts[:cache]`.
|
|
26
38
|
attr_accessor :method, :path, :body, :headers, :opts, :cache
|
|
27
39
|
|
|
28
40
|
# @!visibility private
|
data/lib/parse/client.rb
CHANGED
|
@@ -505,23 +505,44 @@ module Parse
|
|
|
505
505
|
end
|
|
506
506
|
|
|
507
507
|
# @!visibility private
|
|
508
|
-
# Emit a redacted warning about a Parse::Response error
|
|
508
|
+
# Emit a redacted warning about a Parse::Response error.
|
|
509
509
|
#
|
|
510
510
|
# Routes the response error string through
|
|
511
511
|
# {Parse::Middleware::BodyBuilder.redact} to strip credentials (passwords,
|
|
512
512
|
# tokens, sessionTokens, access_tokens, authData) before logging, and
|
|
513
513
|
# truncates to {SAFE_WARN_MAX_ERROR_LENGTH} chars.
|
|
514
514
|
#
|
|
515
|
+
# Writes through {Parse::Middleware::Logging.logger} when the app has
|
|
516
|
+
# configured one (`Parse.logger = ...`), so these warnings land wherever
|
|
517
|
+
# the rest of the app's Parse request/response logging goes instead of
|
|
518
|
+
# bypassing it. Falls back to plain `warn` (STDERR) when no logger is
|
|
519
|
+
# configured, matching prior behavior. Every call site immediately
|
|
520
|
+
# raises the corresponding typed {Parse::Error} right after calling
|
|
521
|
+
# this method, so a misbehaving app-supplied logger (closed handle,
|
|
522
|
+
# full disk, a remote-aggregator client that raises on socket error)
|
|
523
|
+
# must not be allowed to propagate in its place and mask the real
|
|
524
|
+
# error — falls back to `warn` if the logger itself raises.
|
|
525
|
+
#
|
|
515
526
|
# @param tag [String] the bracketed prefix (e.g. "AuthenticationError").
|
|
516
527
|
# @param response [Parse::Response] the response carrying the error.
|
|
517
528
|
# @param name [String, nil] optional cloud-function or job name for context.
|
|
518
529
|
# @return [nil]
|
|
519
530
|
def _safe_warn(tag, response, name: nil)
|
|
520
531
|
err = Parse::Middleware::BodyBuilder.redact(response.error.to_s)[0, SAFE_WARN_MAX_ERROR_LENGTH]
|
|
521
|
-
if name
|
|
522
|
-
|
|
532
|
+
msg = if name
|
|
533
|
+
"[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})"
|
|
534
|
+
else
|
|
535
|
+
"[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})"
|
|
536
|
+
end
|
|
537
|
+
logger = Parse::Middleware::Logging.logger
|
|
538
|
+
if logger
|
|
539
|
+
begin
|
|
540
|
+
logger.warn(msg)
|
|
541
|
+
rescue StandardError
|
|
542
|
+
warn msg
|
|
543
|
+
end
|
|
523
544
|
else
|
|
524
|
-
warn
|
|
545
|
+
warn msg
|
|
525
546
|
end
|
|
526
547
|
nil
|
|
527
548
|
end
|
|
@@ -164,7 +164,15 @@ module Parse
|
|
|
164
164
|
|
|
165
165
|
mime = verify!(bytes, url: canonical)
|
|
166
166
|
bytes = strip_metadata(bytes, mime) if exif_strip
|
|
167
|
-
|
|
167
|
+
# Store the query-stripped URL, not `canonical` verbatim: when
|
|
168
|
+
# `url` is a presigned URL (a private-bucket file adapter),
|
|
169
|
+
# `canonical` carries a live signature, and FetchedImage#url is
|
|
170
|
+
# purely informational from here on (nothing re-fetches it).
|
|
171
|
+
# Keeping the signature out of the struct preserves the
|
|
172
|
+
# log-safety `#inspect` below was written for — third-party
|
|
173
|
+
# provider adapters and error reporters that capture locals
|
|
174
|
+
# would otherwise leak a valid bearer credential through it.
|
|
175
|
+
FetchedImage.new(bytes: bytes, mime_type: mime, url: Parse::File.strip_query(canonical))
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
# Verify raw bytes: sniff the magic, check the allowlist, and
|
data/lib/parse/embeddings.rb
CHANGED
|
@@ -382,6 +382,17 @@ module Parse
|
|
|
382
382
|
# `"true"`, or a non-matching String) raises
|
|
383
383
|
# {ConfirmationRequired}. Reset to `nil` to disable.
|
|
384
384
|
#
|
|
385
|
+
# For a `:file` source backed by a private-bucket adapter
|
|
386
|
+
# (S3/GCS with server-side presigning), the URL forwarded under
|
|
387
|
+
# this sentinel may be the file's presigned URL rather than its
|
|
388
|
+
# bare canonical one — a time-limited bearer credential for that
|
|
389
|
+
# object, not just a pointer to it (see {Parse::File#presigned_url}
|
|
390
|
+
# and `Parse::Core::EmbedManaged.embed_image`). Reviewing the
|
|
391
|
+
# provider's egress behavior before setting this sentinel should
|
|
392
|
+
# account for that: the provider (and anyone with access to its
|
|
393
|
+
# request logs) gains temporary read access to the object for
|
|
394
|
+
# however long the signature remains valid.
|
|
395
|
+
#
|
|
385
396
|
# @param value [String, nil] {TRUST_PROVIDER_URL_FETCH_SENTINEL} or nil.
|
|
386
397
|
# @raise [ConfirmationRequired] on any other value.
|
|
387
398
|
def trust_provider_url_fetch=(value)
|
|
@@ -395,10 +406,12 @@ module Parse
|
|
|
395
406
|
"String #{TRUST_PROVIDER_URL_FETCH_SENTINEL.inspect}. Plain `true` and " \
|
|
396
407
|
"other values are refused — forwarding image URLs to a third-party " \
|
|
397
408
|
"provider lets that provider issue an HTTP request from its own network " \
|
|
398
|
-
"with attacker-controllable host/path
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
409
|
+
"with attacker-controllable host/path, and for a private-bucket file may " \
|
|
410
|
+
"hand it a time-limited presigned URL rather than a bare pointer. Set the " \
|
|
411
|
+
"sentinel only after you have configured " \
|
|
412
|
+
"Parse::Embeddings.allowed_image_hosts AND reviewed the provider's " \
|
|
413
|
+
"documented egress behavior (DNS rebinding window, redirect policy, " \
|
|
414
|
+
"request-log retention)."
|
|
402
415
|
end
|
|
403
416
|
CONFIG_MUTEX.synchronize { @trust_provider_url_fetch = value }
|
|
404
417
|
end
|
|
@@ -243,6 +243,10 @@ module Parse
|
|
|
243
243
|
instance_variable_set ivar, val
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
+
# Capture after lazy hydration, before a caller can mutate an
|
|
247
|
+
# object returned by this association getter.
|
|
248
|
+
send(:_capture_transaction_state!) if respond_to?(:_capture_transaction_state!, true)
|
|
249
|
+
|
|
246
250
|
# Track association source for N+1 detection when returning an unfetched pointer
|
|
247
251
|
# Uses a registry instead of setting instance variables on the pointer object
|
|
248
252
|
if val.is_a?(Parse::Pointer) && val.pointer? && Parse.warn_on_n_plus_one
|
|
@@ -359,6 +359,9 @@ module Parse
|
|
|
359
359
|
|
|
360
360
|
# Notifies the delegate that the collection changed.
|
|
361
361
|
def notify_will_change!
|
|
362
|
+
if @delegate && @delegate.respond_to?(:_capture_transaction_state!, true)
|
|
363
|
+
@delegate.send(:_capture_transaction_state!)
|
|
364
|
+
end
|
|
362
365
|
collection_will_change!
|
|
363
366
|
forward "#{@key}_will_change!"
|
|
364
367
|
end
|
|
@@ -502,6 +502,10 @@ module Parse
|
|
|
502
502
|
val = instance_variable_get ivar
|
|
503
503
|
end
|
|
504
504
|
|
|
505
|
+
# Capture before this getter materializes or returns a mutable
|
|
506
|
+
# proxy that can be changed in place.
|
|
507
|
+
send(:_capture_transaction_state!) if respond_to?(:_capture_transaction_state!, true)
|
|
508
|
+
|
|
505
509
|
# if the result is not a collection proxy, then create a new one.
|
|
506
510
|
unless val.is_a?(Parse::PointerCollectionProxy)
|
|
507
511
|
results = []
|