parse-stack-next 5.0.1 → 5.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
- data/.github/dependabot.yml +13 -0
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +3 -3
- data/.github/workflows/release.yml +14 -3
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +792 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -5
- data/README.md +15 -0
- data/Rakefile +5 -1
- data/docs/acl_clp_guide.md +553 -0
- data/docs/atlas_vector_search_guide.md +123 -22
- data/docs/client_sdk_guide.md +201 -5
- data/docs/usage_guide.md +21 -0
- data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
- data/lib/parse/agent/tools.rb +153 -1
- data/lib/parse/cache/redis.rb +53 -0
- data/lib/parse/client/caching.rb +18 -1
- data/lib/parse/client.rb +79 -12
- data/lib/parse/embeddings/cohere.rb +143 -6
- data/lib/parse/embeddings/provider.rb +20 -2
- data/lib/parse/embeddings/voyage.rb +102 -0
- data/lib/parse/embeddings.rb +332 -1
- data/lib/parse/live_query/client.rb +167 -4
- data/lib/parse/live_query/configuration.rb +12 -0
- data/lib/parse/live_query/subscription.rb +55 -2
- data/lib/parse/live_query.rb +123 -1
- data/lib/parse/lock.rb +342 -0
- data/lib/parse/lock_backend.rb +308 -0
- data/lib/parse/model/classes/audience.rb +5 -0
- data/lib/parse/model/classes/installation.rb +122 -0
- data/lib/parse/model/classes/job_schedule.rb +3 -1
- data/lib/parse/model/classes/job_status.rb +4 -1
- data/lib/parse/model/classes/push_status.rb +4 -1
- data/lib/parse/model/classes/session.rb +7 -0
- data/lib/parse/model/classes/user.rb +204 -0
- data/lib/parse/model/core/create_lock.rb +28 -146
- data/lib/parse/model/core/embed_managed.rb +162 -13
- data/lib/parse/model/core/parse_reference.rb +17 -1
- data/lib/parse/model/core/querying.rb +26 -2
- data/lib/parse/model/file.rb +523 -18
- data/lib/parse/query.rb +31 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +98 -1
- data/parse-stack-next.gemspec +2 -2
- metadata +17 -7
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "monitor"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
# Shared low-level primitives for both {Parse::CreateLock} (the
|
|
9
|
+
# internal lock used by `first_or_create!` / `create_or_update!`)
|
|
10
|
+
# and {Parse::Lock} (the public mutual-exclusion primitive). The
|
|
11
|
+
# extraction exists so {Parse::Lock} does not reach into
|
|
12
|
+
# {Parse::CreateLock}'s private methods via `.send` — a brittle
|
|
13
|
+
# coupling pattern called out in the v5.1.0 round-2 review. Both
|
|
14
|
+
# callers now depend on a small documented surface and any future
|
|
15
|
+
# refactor of the lock store discovery / degraded-detection
|
|
16
|
+
# heuristic / atomic-SETNX semantics happens in exactly one place.
|
|
17
|
+
#
|
|
18
|
+
# **Not a public API for application code.** `@!visibility private`
|
|
19
|
+
# is intentional. End users compose with locking through
|
|
20
|
+
# {Parse::Lock.acquire} (block-form) or `first_or_create!` /
|
|
21
|
+
# `create_or_update!` (find-or-create). This module is documented
|
|
22
|
+
# only because SDK extension authors and security auditors need to
|
|
23
|
+
# know where the SETNX semantics actually live.
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
module LockBackend
|
|
27
|
+
# Base poll interval for the wait-loop spin in the caller's
|
|
28
|
+
# acquire loop. Caller adds jitter via {.poll_interval}; this
|
|
29
|
+
# constant is the midpoint.
|
|
30
|
+
DEFAULT_POLL_BASE = 0.05
|
|
31
|
+
|
|
32
|
+
# Half-width of the symmetric jitter applied around
|
|
33
|
+
# {DEFAULT_POLL_BASE}. Spreads contended-acquire spin starts so
|
|
34
|
+
# N waiters don't all hit `try_acquire` on the same monotonic
|
|
35
|
+
# tick after a release.
|
|
36
|
+
DEFAULT_POLL_JITTER = 0.015
|
|
37
|
+
|
|
38
|
+
# Throttle floor for {.handle_degraded}(`:warn_throttled`). One
|
|
39
|
+
# warning per process per this many seconds; subsequent degraded
|
|
40
|
+
# acquisitions are silent.
|
|
41
|
+
DEGRADED_WARNING_THROTTLE_SECONDS = 60
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Find the Moneta store the lock should write through. Resolved
|
|
45
|
+
# at call time (not memoized) so a test or an operator can swap
|
|
46
|
+
# `Parse.synchronize_create_store` after boot and see the change
|
|
47
|
+
# take effect on the next acquisition.
|
|
48
|
+
#
|
|
49
|
+
# @return [Object, nil] a Moneta-shaped store, or nil when none
|
|
50
|
+
# is configured / the Parse client is unconfigured.
|
|
51
|
+
def lock_store
|
|
52
|
+
if Parse.respond_to?(:synchronize_create_store) && Parse.synchronize_create_store
|
|
53
|
+
return Parse.synchronize_create_store
|
|
54
|
+
end
|
|
55
|
+
Parse.cache
|
|
56
|
+
rescue Parse::Error::ConnectionError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Decide whether `store` is process-local (Memory / Null /
|
|
61
|
+
# missing-`:create` / nil) — i.e. cannot serve as a cross-
|
|
62
|
+
# process lock store, so the caller should fall back to a
|
|
63
|
+
# per-key in-process Mutex. The {Parse::Cache::Redis} wrapper
|
|
64
|
+
# is explicitly accepted because it doesn't expose a Moneta
|
|
65
|
+
# `.adapter` chain to walk.
|
|
66
|
+
#
|
|
67
|
+
# @param store [Object, nil] candidate store
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def degraded_store?(store)
|
|
70
|
+
return true if store.nil?
|
|
71
|
+
return false if defined?(Parse::Cache::Redis) && store.is_a?(Parse::Cache::Redis)
|
|
72
|
+
return true unless store.respond_to?(:create)
|
|
73
|
+
bottom = walk_to_adapter(store)
|
|
74
|
+
return true if bottom.nil?
|
|
75
|
+
klass_name = bottom.class.name.to_s
|
|
76
|
+
klass_name.include?("Memory") || klass_name.include?("Null")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Emit the configured degraded-store warning. `source:` lets
|
|
80
|
+
# the caller tag the prefix so an operator reading
|
|
81
|
+
# `[Parse::Lock] Lock store is process-local` knows which
|
|
82
|
+
# caller surfaced the warning (vs `[Parse::CreateLock]` for
|
|
83
|
+
# the find-or-create path).
|
|
84
|
+
#
|
|
85
|
+
# @param mode [Symbol] one of `:warn`, `:warn_throttled`,
|
|
86
|
+
# `:proceed`, `:raise`. Callers are responsible for
|
|
87
|
+
# validating the symbol BEFORE calling this; an unknown
|
|
88
|
+
# value here falls through to plain `:warn`.
|
|
89
|
+
# @param key [String] the (already-hashed) cache key — used
|
|
90
|
+
# only for the debug snippet in the warning message.
|
|
91
|
+
# @param source [String] caller tag for the log prefix.
|
|
92
|
+
# @param unavailable_error [Class] error class to raise in
|
|
93
|
+
# `:raise` mode. Lets each caller raise its own typed
|
|
94
|
+
# error (Parse::CreateLockUnavailableError vs
|
|
95
|
+
# Parse::Lock::UnavailableError) without coupling here.
|
|
96
|
+
def handle_degraded(mode, key, source: "Parse::LockBackend",
|
|
97
|
+
unavailable_error: nil)
|
|
98
|
+
case mode
|
|
99
|
+
when :raise
|
|
100
|
+
err = unavailable_error || Parse::Error
|
|
101
|
+
raise err,
|
|
102
|
+
"#{source}: cross-process lock store unavailable; " \
|
|
103
|
+
"current store is process-local"
|
|
104
|
+
when :proceed
|
|
105
|
+
# silent
|
|
106
|
+
when :warn_throttled
|
|
107
|
+
now = monotonic_now
|
|
108
|
+
if @degraded_warned_at.nil? ||
|
|
109
|
+
(now - @degraded_warned_at) >= DEGRADED_WARNING_THROTTLE_SECONDS
|
|
110
|
+
@degraded_warned_at = now
|
|
111
|
+
warn "[#{source}] Lock store is process-local (Moneta Memory/Null). " \
|
|
112
|
+
"Cross-process locking is NOT in effect. Configure a Redis-backed " \
|
|
113
|
+
"cache to enable distributed locking."
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
warn "[#{source}] Lock store is process-local; cross-process locking disabled. " \
|
|
117
|
+
"key_digest=#{key.is_a?(String) ? key[-12..] : key.inspect}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Atomic SETNX-style acquisition. Returns true on success,
|
|
122
|
+
# false on contention OR error (logged). Never raises — the
|
|
123
|
+
# caller's wait loop is the source of truth for "did we get
|
|
124
|
+
# the lock," and a transient store error should look the
|
|
125
|
+
# same to the loop as "someone else has it."
|
|
126
|
+
#
|
|
127
|
+
# @param store [Object] Moneta-shaped store responding to
|
|
128
|
+
# `:create` and `:key?`.
|
|
129
|
+
# @param key [String] cache key (already prefixed/hashed).
|
|
130
|
+
# @param owner [String] unique-per-acquisition identifier
|
|
131
|
+
# used by {.release}'s compare-and-delete.
|
|
132
|
+
# @param ttl [Integer] seconds before the store entry self-
|
|
133
|
+
# clears (crash-recovery floor).
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def try_acquire(store, key, owner, ttl)
|
|
136
|
+
# Prefer the store's native atomic lock primitive when it exposes
|
|
137
|
+
# one (Parse::Cache::Redis). That path uses raw-Redis
|
|
138
|
+
# `SET key owner NX EX ttl` with plain-string encoding so it pairs
|
|
139
|
+
# with the atomic compare-and-delete in {.release}. Falls back to
|
|
140
|
+
# Moneta `:create` (also an atomic SETNX) for raw-Moneta stores.
|
|
141
|
+
return store.lock_acquire(key, owner, ttl) if store.respond_to?(:lock_acquire)
|
|
142
|
+
|
|
143
|
+
# Trigger lazy TTL sweep on Moneta::Memory before `:create`
|
|
144
|
+
# (no-op on Redis). Without this, the Memory adapter returns
|
|
145
|
+
# false on `:create` even after TTL expiry until a `:key?`
|
|
146
|
+
# or `:[]` access flushes the stale entry.
|
|
147
|
+
store.key?(key)
|
|
148
|
+
store.create(key, owner, expires: ttl)
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
warn "[Parse::LockBackend] acquire error (#{e.class}): #{e.message}"
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Compare-and-delete release. When the store exposes an atomic
|
|
155
|
+
# primitive (Parse::Cache::Redis → server-side Lua CAD), use it so
|
|
156
|
+
# a holder whose lease expired and was re-acquired by someone else
|
|
157
|
+
# can never delete the new holder's key. Falls back to a
|
|
158
|
+
# best-effort GET-then-DEL for raw-Moneta stores, where the
|
|
159
|
+
# worst-case cross-holder-delete race is bounded by the short TTL
|
|
160
|
+
# (callers clamp `ttl:` to ≤ 30s) — documented residual risk for
|
|
161
|
+
# the non-Redis path.
|
|
162
|
+
#
|
|
163
|
+
# @param store [Object] Moneta-shaped store.
|
|
164
|
+
# @param key [String] cache key.
|
|
165
|
+
# @param owner [String] the owner token from {.try_acquire}.
|
|
166
|
+
def release(store, key, owner)
|
|
167
|
+
return store.lock_release(key, owner) if store.respond_to?(:lock_release)
|
|
168
|
+
|
|
169
|
+
current = store[key]
|
|
170
|
+
store.delete(key) if current == owner
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
warn "[Parse::LockBackend] release error (#{e.class}): #{e.message}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Jittered poll interval for the wait-loop. Symmetric jitter
|
|
176
|
+
# around {DEFAULT_POLL_BASE} of half-width
|
|
177
|
+
# {DEFAULT_POLL_JITTER}; the result is bounded but
|
|
178
|
+
# non-deterministic so contended waiters don't sync up.
|
|
179
|
+
#
|
|
180
|
+
# @return [Float] seconds.
|
|
181
|
+
def poll_interval
|
|
182
|
+
DEFAULT_POLL_BASE + (rand * 2 - 1) * DEFAULT_POLL_JITTER
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Per-key in-process Mutex registry for the degraded fallback
|
|
186
|
+
# path. The first acquisition for a given key creates the
|
|
187
|
+
# Mutex; subsequent acquisitions reuse it. Registry itself is
|
|
188
|
+
# guarded by a tiny outer Mutex so two threads racing the
|
|
189
|
+
# first acquisition of the same key get the same Mutex.
|
|
190
|
+
#
|
|
191
|
+
# @param key [String] cache key.
|
|
192
|
+
# @return [Mutex]
|
|
193
|
+
def process_mutex(key)
|
|
194
|
+
@process_mutex_registry_lock ||= Mutex.new
|
|
195
|
+
@process_mutex_registry_lock.synchronize do
|
|
196
|
+
@process_mutex_registry ||= {}
|
|
197
|
+
@process_mutex_registry[key] ||= Mutex.new
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [Float] CLOCK_MONOTONIC seconds.
|
|
202
|
+
def monotonic_now
|
|
203
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Reset backend-owned state. Intended for test teardown —
|
|
207
|
+
# production code should never call this.
|
|
208
|
+
#
|
|
209
|
+
# @return [void]
|
|
210
|
+
def reset!
|
|
211
|
+
@degraded_warned_at = nil
|
|
212
|
+
@process_mutex_registry = nil
|
|
213
|
+
@process_mutex_registry_lock = nil
|
|
214
|
+
@auto_secret = nil
|
|
215
|
+
@plain_sha_warned = nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# =====================================================================
|
|
219
|
+
# HMAC secret resolution (v5.1.0 — extracted from Parse::CreateLock so
|
|
220
|
+
# Parse::Lock can also derive HMAC-keyed cache keys when an operator-
|
|
221
|
+
# configured secret is available)
|
|
222
|
+
# =====================================================================
|
|
223
|
+
|
|
224
|
+
# Resolve the HMAC secret for lock-key derivation. Behavior depends
|
|
225
|
+
# on store type:
|
|
226
|
+
#
|
|
227
|
+
# * **Configured secret present** — returned verbatim (operator
|
|
228
|
+
# set `Parse.synchronize_create_secret` or
|
|
229
|
+
# `PARSE_STACK_LOCK_SECRET`).
|
|
230
|
+
# * **Degraded (process-local) store, no configured secret** —
|
|
231
|
+
# returns the per-process auto-derived random secret. Locking
|
|
232
|
+
# is already process-local in this branch, so a per-process
|
|
233
|
+
# secret is fine and improves test/single-process privacy by
|
|
234
|
+
# preventing `KEYS *` enumeration.
|
|
235
|
+
# * **Cross-process store, no configured secret** — returns `nil`
|
|
236
|
+
# with a one-time warn. Per-process auto-derived secrets would
|
|
237
|
+
# break cross-process key equality (and therefore the lock
|
|
238
|
+
# itself), so the caller falls back to plain SHA-256 and gets a
|
|
239
|
+
# loud nudge to configure a real secret.
|
|
240
|
+
#
|
|
241
|
+
# @param store [Object, nil] the lock store (used only for
|
|
242
|
+
# degraded detection).
|
|
243
|
+
# @param source [String] caller tag for the warn-once message —
|
|
244
|
+
# "Parse::CreateLock" or "Parse::Lock".
|
|
245
|
+
# @return [String, nil] the secret, or nil to indicate plain SHA.
|
|
246
|
+
def lock_secret_for(store:, source: "Parse::LockBackend")
|
|
247
|
+
configured = configured_secret
|
|
248
|
+
return configured if configured && !configured.empty?
|
|
249
|
+
if degraded_store?(store)
|
|
250
|
+
auto_secret
|
|
251
|
+
else
|
|
252
|
+
warn_plain_sha_once(source: source)
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# @return [String, nil] operator-configured HMAC secret from
|
|
258
|
+
# `Parse.synchronize_create_secret` or
|
|
259
|
+
# `PARSE_STACK_LOCK_SECRET`. The env-var and accessor names
|
|
260
|
+
# carry "synchronize_create" / "LOCK" historical naming;
|
|
261
|
+
# both `Parse::CreateLock` and `Parse::Lock` consume the same
|
|
262
|
+
# value.
|
|
263
|
+
def configured_secret
|
|
264
|
+
if Parse.respond_to?(:synchronize_create_secret) && Parse.synchronize_create_secret
|
|
265
|
+
return Parse.synchronize_create_secret.to_s
|
|
266
|
+
end
|
|
267
|
+
ENV["PARSE_STACK_LOCK_SECRET"]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @return [String] per-process random secret. Memoized.
|
|
271
|
+
def auto_secret
|
|
272
|
+
@auto_secret ||= SecureRandom.hex(32)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# One-time process-scoped warn when a cross-process lock store
|
|
276
|
+
# is in use without an operator-configured HMAC secret. The
|
|
277
|
+
# warning text explains both the enumeration risk (key material
|
|
278
|
+
# is deterministic) and the lock-pinning risk (when the cache
|
|
279
|
+
# and lock store share a Redis DB) and points at the
|
|
280
|
+
# remediation knobs.
|
|
281
|
+
def warn_plain_sha_once(source:)
|
|
282
|
+
return if @plain_sha_warned
|
|
283
|
+
@plain_sha_warned = true
|
|
284
|
+
warn "[#{source}:SECURITY] No PARSE_STACK_LOCK_SECRET configured and Redis-backed store detected. " \
|
|
285
|
+
"Falling back to plain SHA256 for lock-key derivation so cross-process locking actually works. " \
|
|
286
|
+
"Risks of running without an HMAC secret: (1) lock keys are deterministic and may expose key " \
|
|
287
|
+
"material content via Redis MONITOR/snapshots; (2) when the response cache and the lock store " \
|
|
288
|
+
"share a Redis DB, any caller with write access to Parse.cache can plant a lock key under a " \
|
|
289
|
+
"guessable digest and pin the lock for that resource until TTL expiry — a targeted DoS / " \
|
|
290
|
+
"lock-pinning primitive. Set PARSE_STACK_LOCK_SECRET (or Parse.synchronize_create_secret = '…') " \
|
|
291
|
+
"to enable HMAC keying, or point Parse.synchronize_create_store at a separate Redis DB from " \
|
|
292
|
+
"the response cache."
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
# Walk the Moneta transformer chain to the bottom adapter so
|
|
298
|
+
# {.degraded_store?} can name-check the adapter class.
|
|
299
|
+
def walk_to_adapter(store)
|
|
300
|
+
current = store
|
|
301
|
+
while current.respond_to?(:adapter) && current.adapter && current.adapter != current
|
|
302
|
+
current = current.adapter
|
|
303
|
+
end
|
|
304
|
+
current
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -50,6 +50,11 @@ module Parse
|
|
|
50
50
|
# .with_alert("Exclusive offer!")
|
|
51
51
|
# .send!
|
|
52
52
|
#
|
|
53
|
+
# @note `_Audience` is hardcoded master-key-only at Parse Server's
|
|
54
|
+
# REST layer (`SharedRest.js`). CLP changes via
|
|
55
|
+
# {Parse::Object.set_clp} have no effect — manage audiences from a
|
|
56
|
+
# master-key client or expose them through a Cloud Code function.
|
|
57
|
+
#
|
|
53
58
|
# @see Parse::Push#to_audience
|
|
54
59
|
# @see Parse::Object
|
|
55
60
|
class Audience < Parse::Object
|
|
@@ -35,10 +35,117 @@ module Parse
|
|
|
35
35
|
#
|
|
36
36
|
# has_one :session, ->{ where(installation_id: i.installation_id) }, scope_only: true
|
|
37
37
|
# end
|
|
38
|
+
# ## Class-Level Permissions on `_Installation`
|
|
39
|
+
#
|
|
40
|
+
# `_Installation` is special-cased inside Parse Server. Some operations are
|
|
41
|
+
# hardcoded at the REST layer and CANNOT be relaxed via CLP — calling
|
|
42
|
+
# {Parse::Object.set_clp} for them has no effect on the server's actual
|
|
43
|
+
# behavior, regardless of what you pass. Other operations work the way
|
|
44
|
+
# CLP normally does. The matrix:
|
|
45
|
+
#
|
|
46
|
+
# | Operation | Behavior |
|
|
47
|
+
# |------------|-------------------------------------------------------------------------------------------|
|
|
48
|
+
# | `find` | **Master key only. Hardcoded.** `set_clp :find, ...` is effectively ignored by the server. |
|
|
49
|
+
# | `delete` | **Master key only. Hardcoded.** `set_clp :delete, ...` is effectively ignored by the server. |
|
|
50
|
+
# | `create` | Open to anonymous clients (the `X-Parse-Installation-Id` header is the credential). Locking via CLP breaks first-launch device registration. |
|
|
51
|
+
# | `update` | Open to clients whose `installationId` matches the record; else master key. Locking via CLP breaks silent device-token refresh and channel subscribe/unsubscribe before login. |
|
|
52
|
+
# | `get` | CLP applies normally. Safe to tighten — SDKs don't usually GET their own installation from the server. |
|
|
53
|
+
# | `count` | CLP applies normally. Safe to tighten to master-only (the push flow doesn't need it). |
|
|
54
|
+
# | `addField` | CLP applies normally. Safe to tighten to master-only as a hardening default. |
|
|
55
|
+
#
|
|
56
|
+
# ### What you can safely do with `set_clp` on `_Installation`
|
|
57
|
+
#
|
|
58
|
+
# * `set_clp :get, requires_authentication: true` (or `{}` for master-only)
|
|
59
|
+
# * `set_clp :count` (master-only)
|
|
60
|
+
# * `set_clp :addField` (master-only)
|
|
61
|
+
# * {Parse::Object.protect_fields} to hide `device_token`, `gcm_sender_id`,
|
|
62
|
+
# `push_type`, etc. from non-master reads — these are write-only from the
|
|
63
|
+
# client's perspective in normal SDK flows.
|
|
64
|
+
#
|
|
65
|
+
# ### What you should NOT do with `set_clp` on `_Installation`
|
|
66
|
+
#
|
|
67
|
+
# * `set_clp :create, requires_authentication: true` — breaks device
|
|
68
|
+
# registration for users who haven't logged in yet.
|
|
69
|
+
# * `set_clp :update, requires_authentication: true` — breaks background
|
|
70
|
+
# device-token refresh and pre-login channel subscribe/unsubscribe.
|
|
71
|
+
# * Pointer-based `set_read_user_fields` / `set_write_user_fields` —
|
|
72
|
+
# an installation has no stable owning user (a device can outlive a user
|
|
73
|
+
# session and change users), so user-pointer ACLing is unreliable here.
|
|
74
|
+
# * `set_clp :find, public: true` (or any other `:find` config) —
|
|
75
|
+
# has no effect; the server enforces master-only at the REST layer.
|
|
76
|
+
#
|
|
77
|
+
# If your app actually does require login before any installation write,
|
|
78
|
+
# put that policy in a `beforeSave('_Installation')` Cloud Code trigger
|
|
79
|
+
# rather than in CLP — the trigger fires under master-key context and can
|
|
80
|
+
# inspect `request.user` directly without breaking the SDK's anonymous
|
|
81
|
+
# registration handshake.
|
|
82
|
+
#
|
|
38
83
|
# @see Push
|
|
39
84
|
# @see Parse::Object
|
|
40
85
|
class Installation < Parse::Object
|
|
41
86
|
parse_class Parse::Model::CLASS_INSTALLATION
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
# Override {Parse::Object.set_clp} on `_Installation` so that any
|
|
90
|
+
# attempt to change CLP from the SDK emits a one-time advisory.
|
|
91
|
+
# Parse Server hardcodes `find` and `delete` on `_Installation` to
|
|
92
|
+
# master-key-only at the REST layer, and gates `create`/`update`
|
|
93
|
+
# on the `X-Parse-Installation-Id` header rather than CLP — so
|
|
94
|
+
# most CLP changes here either do nothing or break the SDK's
|
|
95
|
+
# device-registration flow. Behavior is otherwise unchanged.
|
|
96
|
+
def set_clp(operation, **opts)
|
|
97
|
+
_warn_about_installation_clp!(:set_clp, operation)
|
|
98
|
+
super
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Same advisory for the bulk-config DSL.
|
|
102
|
+
def set_class_access(**ops_to_access)
|
|
103
|
+
_warn_about_installation_clp!(:set_class_access, ops_to_access.keys)
|
|
104
|
+
super
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `protect_fields` on `_Installation` is a documented-legitimate use
|
|
108
|
+
# (e.g. hiding `device_token` / `gcm_sender_id` / `push_type` from
|
|
109
|
+
# non-master reads), so we deliberately do NOT fire the
|
|
110
|
+
# find/delete-are-hardcoded advisory here. The advisory exists to
|
|
111
|
+
# nudge callers away from CLP changes that the server ignores;
|
|
112
|
+
# protectedFields is one of the four operations on _Installation
|
|
113
|
+
# that CLP actually controls.
|
|
114
|
+
def protect_fields(pattern, fields)
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Pointer-permission helpers on `_Installation` are a mistake in
|
|
119
|
+
# practice (devices have no stable owning user); warn loudly.
|
|
120
|
+
def set_read_user_fields(*fields)
|
|
121
|
+
_warn_about_installation_clp!(:set_read_user_fields, fields)
|
|
122
|
+
super
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def set_write_user_fields(*fields)
|
|
126
|
+
_warn_about_installation_clp!(:set_write_user_fields, fields)
|
|
127
|
+
super
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @!visibility private
|
|
131
|
+
def _warn_about_installation_clp!(method, detail)
|
|
132
|
+
return if @_installation_clp_warned
|
|
133
|
+
@_installation_clp_warned = true
|
|
134
|
+
msg = "[Parse::Installation] #{method}(#{Array(detail).inspect}) on _Installation: " \
|
|
135
|
+
"Parse Server hardcodes find/delete on _Installation to master-key-only " \
|
|
136
|
+
"(CLP changes for those operations are ignored), and gates create/update " \
|
|
137
|
+
"on the X-Parse-Installation-Id header rather than CLP. Only get, count, " \
|
|
138
|
+
"addField, and protectedFields actually respond to CLP here. " \
|
|
139
|
+
"If you need login-required writes, use a beforeSave('_Installation') " \
|
|
140
|
+
"Cloud Code trigger instead. See Parse::Installation docs and " \
|
|
141
|
+
"docs/client_sdk_guide.md §6.3."
|
|
142
|
+
if Parse.respond_to?(:logger) && Parse.logger
|
|
143
|
+
Parse.logger.warn(msg)
|
|
144
|
+
else
|
|
145
|
+
Kernel.warn(msg)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
42
149
|
# @!attribute gcm_sender_id
|
|
43
150
|
# This field only has meaning for Android installations that use the GCM
|
|
44
151
|
# push type. It is reserved for directing Parse to send pushes to this
|
|
@@ -135,6 +242,21 @@ module Parse
|
|
|
135
242
|
# @return [Parse::Session] The associated {Parse::Session} that might be tied to this installation
|
|
136
243
|
has_one :session, -> { where(installation_id: i.installation_id) }, scope_only: true
|
|
137
244
|
|
|
245
|
+
# @!attribute user
|
|
246
|
+
# The {Parse::User} associated with this installation. Parse Server
|
|
247
|
+
# populates this pointer when the installation is created or updated
|
|
248
|
+
# by an authenticated client (the session-token holder on the
|
|
249
|
+
# request). It is useful for targeted push delivery — finding all
|
|
250
|
+
# installations belonging to a given user.
|
|
251
|
+
#
|
|
252
|
+
# **Caveat — do not use for ACL or CLP scoping.** Devices outlive
|
|
253
|
+
# sessions and can change users (account switch, sign-out, shared
|
|
254
|
+
# device), so the `user` pointer on `_Installation` is not a
|
|
255
|
+
# reliable owner identity. See the "What you should NOT do with
|
|
256
|
+
# `set_clp`" notes above for the broader context.
|
|
257
|
+
# @return [Parse::User]
|
|
258
|
+
belongs_to :user
|
|
259
|
+
|
|
138
260
|
# =========================================================================
|
|
139
261
|
# Channel Management - Class Methods
|
|
140
262
|
# =========================================================================
|
|
@@ -59,7 +59,9 @@ module Parse
|
|
|
59
59
|
# @note This collection is consumed by external scheduling tooling, not by
|
|
60
60
|
# Parse Server itself. {#params} is stored as a JSON string (not an
|
|
61
61
|
# Object) per the canonical Parse Server schema; use {#parsed_params} to
|
|
62
|
-
# decode.
|
|
62
|
+
# decode. `_JobSchedule` is hardcoded master-key-only at Parse Server's
|
|
63
|
+
# REST layer (`SharedRest.js`) — CLP changes via
|
|
64
|
+
# {Parse::Object.set_clp} have no effect.
|
|
63
65
|
# @see Parse::JobStatus
|
|
64
66
|
# @see Parse::Object
|
|
65
67
|
class JobSchedule < Parse::Object
|
|
@@ -70,7 +70,10 @@ module Parse
|
|
|
70
70
|
# Parse::JobStatus.failed.where(:created_at.gt => yesterday).all
|
|
71
71
|
#
|
|
72
72
|
# @note This collection is written by Parse Server itself and read access
|
|
73
|
-
#
|
|
73
|
+
# requires the master key. `_JobStatus` is hardcoded master-key-only at
|
|
74
|
+
# Parse Server's REST layer (`SharedRest.js`) — CLP changes via
|
|
75
|
+
# {Parse::Object.set_clp} have no effect. Use a master-key client (or a
|
|
76
|
+
# Cloud Code function) to read it. Parse Server does not garbage-collect
|
|
74
77
|
# `_JobStatus` rows — long-running deployments accumulate history and
|
|
75
78
|
# should implement their own retention policy.
|
|
76
79
|
# @see Parse::JobSchedule for the corresponding scheduled-run configuration.
|
|
@@ -43,7 +43,10 @@ module Parse
|
|
|
43
43
|
# recent = Parse::PushStatus.recent.limit(10).all
|
|
44
44
|
# recent.each { |s| puts "#{s.status}: #{s.num_sent} sent" }
|
|
45
45
|
#
|
|
46
|
-
# @note
|
|
46
|
+
# @note `_PushStatus` is hardcoded master-key-only at Parse Server's
|
|
47
|
+
# REST layer (`SharedRest.js`). CLP changes via
|
|
48
|
+
# {Parse::Object.set_clp} have no effect — all reads and writes
|
|
49
|
+
# require a master-key client.
|
|
47
50
|
# @see Parse::Push
|
|
48
51
|
# @see Parse::Object
|
|
49
52
|
class PushStatus < Parse::Object
|
|
@@ -27,6 +27,13 @@ module Parse
|
|
|
27
27
|
# has_one :installation, ->{ where(installation_id: i.installation_id) }, scope_only: true
|
|
28
28
|
# end
|
|
29
29
|
#
|
|
30
|
+
# @note CLP on `_Session` is mostly redundant: non-master `find` queries
|
|
31
|
+
# are silently rewritten by Parse Server's REST layer
|
|
32
|
+
# (`RestQuery.js`) to scope by `user = <current user>`, so a caller
|
|
33
|
+
# never sees another user's sessions regardless of CLP. `find` also
|
|
34
|
+
# requires a session token. You cannot grant cross-user session
|
|
35
|
+
# visibility through {Parse::Object.set_clp}.
|
|
36
|
+
#
|
|
30
37
|
# @see Parse::Object
|
|
31
38
|
class Session < Parse::Object
|
|
32
39
|
parse_class Parse::Model::CLASS_SESSION
|