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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
  4. data/.github/dependabot.yml +13 -0
  5. data/.github/workflows/codeql.yml +1 -1
  6. data/.github/workflows/docs.yml +3 -3
  7. data/.github/workflows/release.yml +14 -3
  8. data/.github/workflows/ruby.yml +1 -1
  9. data/.gitignore +1 -0
  10. data/.yardopts +19 -0
  11. data/CHANGELOG.md +792 -0
  12. data/Gemfile +3 -0
  13. data/Gemfile.lock +8 -5
  14. data/README.md +15 -0
  15. data/Rakefile +5 -1
  16. data/docs/acl_clp_guide.md +553 -0
  17. data/docs/atlas_vector_search_guide.md +123 -22
  18. data/docs/client_sdk_guide.md +201 -5
  19. data/docs/usage_guide.md +21 -0
  20. data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
  21. data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
  22. data/lib/parse/agent/tools.rb +153 -1
  23. data/lib/parse/cache/redis.rb +53 -0
  24. data/lib/parse/client/caching.rb +18 -1
  25. data/lib/parse/client.rb +79 -12
  26. data/lib/parse/embeddings/cohere.rb +143 -6
  27. data/lib/parse/embeddings/provider.rb +20 -2
  28. data/lib/parse/embeddings/voyage.rb +102 -0
  29. data/lib/parse/embeddings.rb +332 -1
  30. data/lib/parse/live_query/client.rb +167 -4
  31. data/lib/parse/live_query/configuration.rb +12 -0
  32. data/lib/parse/live_query/subscription.rb +55 -2
  33. data/lib/parse/live_query.rb +123 -1
  34. data/lib/parse/lock.rb +342 -0
  35. data/lib/parse/lock_backend.rb +308 -0
  36. data/lib/parse/model/classes/audience.rb +5 -0
  37. data/lib/parse/model/classes/installation.rb +122 -0
  38. data/lib/parse/model/classes/job_schedule.rb +3 -1
  39. data/lib/parse/model/classes/job_status.rb +4 -1
  40. data/lib/parse/model/classes/push_status.rb +4 -1
  41. data/lib/parse/model/classes/session.rb +7 -0
  42. data/lib/parse/model/classes/user.rb +204 -0
  43. data/lib/parse/model/core/create_lock.rb +28 -146
  44. data/lib/parse/model/core/embed_managed.rb +162 -13
  45. data/lib/parse/model/core/parse_reference.rb +17 -1
  46. data/lib/parse/model/core/querying.rb +26 -2
  47. data/lib/parse/model/file.rb +523 -18
  48. data/lib/parse/query.rb +31 -1
  49. data/lib/parse/stack/version.rb +1 -1
  50. data/lib/parse/stack.rb +98 -1
  51. data/parse-stack-next.gemspec +2 -2
  52. 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. Master-key access is typically required.
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
- # typically requires the master key. Parse Server does not garbage-collect
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 This collection requires master key access
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