parse-stack-next 5.8.0 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +318 -0
- data/README.md +25 -19
- data/docs/caching.md +50 -0
- data/docs/mcp_guide.md +15 -3
- data/docs/webhooks_guide.md +59 -7
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent.rb +205 -20
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +49 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +35 -2
- data/lib/parse/client.rb +26 -2
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +279 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +19 -4
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +19 -1
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +31 -27
- data/lib/parse/query.rb +793 -55
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +19 -0
- data/lib/parse/vector_search/hybrid.rb +74 -15
- data/lib/parse/vector_search.rb +214 -7
- data/lib/parse/webhooks/payload.rb +43 -0
- data/lib/parse/webhooks.rb +312 -9
- metadata +1 -1
|
@@ -374,7 +374,11 @@ module Parse
|
|
|
374
374
|
if class_name.is_a?(Parse::Query)
|
|
375
375
|
query = class_name
|
|
376
376
|
class_name = query.table
|
|
377
|
-
where = query.
|
|
377
|
+
where = query.compile_rest_where
|
|
378
|
+
# The subscription runs under the query's own session (or its
|
|
379
|
+
# `become` client's); a different explicit token raises, and so
|
|
380
|
+
# does a query bound to another application than this client.
|
|
381
|
+
session_token = query.live_query_session_token(session_token, live_query_client: self)
|
|
378
382
|
end
|
|
379
383
|
|
|
380
384
|
# Refuse server-side-JS / data-mutating operators in the `where`
|
|
@@ -1007,6 +1011,27 @@ module Parse
|
|
|
1007
1011
|
end
|
|
1008
1012
|
end
|
|
1009
1013
|
|
|
1014
|
+
# Release the latch set when this admin client first connected inside
|
|
1015
|
+
# {Parse.without_master_key}. Until this is called, every later
|
|
1016
|
+
# connect and reconnect withholds the master key. The next connect
|
|
1017
|
+
# made outside the block then sends it again, elevating every
|
|
1018
|
+
# subscription on the socket (a warning names how many).
|
|
1019
|
+
# @return [void]
|
|
1020
|
+
def allow_master_key_connection!
|
|
1021
|
+
return unless @master_key_withheld
|
|
1022
|
+
@master_key_withheld = false
|
|
1023
|
+
@master_key_latch_released = true
|
|
1024
|
+
nil
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
# @return [Boolean] whether this client withholds its master key on
|
|
1028
|
+
# every connect because it first connected inside
|
|
1029
|
+
# {Parse.without_master_key}.
|
|
1030
|
+
def master_key_withheld?
|
|
1031
|
+
@master_key_withheld == true
|
|
1032
|
+
end
|
|
1033
|
+
public :allow_master_key_connection!, :master_key_withheld?
|
|
1034
|
+
|
|
1010
1035
|
# Resubscribe all pending subscriptions
|
|
1011
1036
|
def resubscribe_all
|
|
1012
1037
|
subs = @monitor.synchronize { @subscriptions.values.dup }
|
|
@@ -1031,8 +1056,32 @@ module Parse
|
|
|
1031
1056
|
# bypasses ACL/CLP/protectedFields. Sending it unconditionally
|
|
1032
1057
|
# (the pre-5.1.0 behavior) silently elevated session-token
|
|
1033
1058
|
# subscriptions the caller believed were scoped.
|
|
1034
|
-
if admin_connection?
|
|
1059
|
+
if admin_connection? && Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled?
|
|
1060
|
+
# Inside `Parse.without_master_key` the connect frame carries no
|
|
1061
|
+
# master key, as REST requests in the block do not. The socket is
|
|
1062
|
+
# then ACL-scoped for its whole lifetime, and the choice is latched:
|
|
1063
|
+
# a later reconnect (including an explicit `connect` made outside
|
|
1064
|
+
# the block) keeps withholding it, so subscriptions created on
|
|
1065
|
+
# this socket are never silently elevated.
|
|
1066
|
+
@master_key_withheld = true
|
|
1067
|
+
@connected_as_admin = false
|
|
1068
|
+
warn_master_key_withheld_once
|
|
1069
|
+
elsif admin_connection? && @master_key_withheld
|
|
1070
|
+
@connected_as_admin = false
|
|
1071
|
+
warn_master_key_withheld_once
|
|
1072
|
+
elsif admin_connection?
|
|
1073
|
+
if @master_key_latch_released
|
|
1074
|
+
@master_key_latch_released = false
|
|
1075
|
+
pending = @monitor.synchronize { @subscriptions.size }
|
|
1076
|
+
if pending.positive?
|
|
1077
|
+
warn "[Parse::LiveQuery:SECURITY] reconnecting with the master key after " \
|
|
1078
|
+
"allow_master_key_connection!: #{pending} existing subscription(s), " \
|
|
1079
|
+
"including any created inside Parse.without_master_key, now bypass " \
|
|
1080
|
+
"ACL/CLP."
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1035
1083
|
message[:masterKey] = @master_key
|
|
1084
|
+
@connected_as_admin = true
|
|
1036
1085
|
warn_master_key_connection_once
|
|
1037
1086
|
elsif @use_master_key
|
|
1038
1087
|
# Opted into admin mode but no usable master key is present —
|
|
@@ -1045,6 +1094,16 @@ module Parse
|
|
|
1045
1094
|
send_message(message)
|
|
1046
1095
|
end
|
|
1047
1096
|
|
|
1097
|
+
# One-time warning that an admin connection was opened without its
|
|
1098
|
+
# master key because `Parse.without_master_key` was active.
|
|
1099
|
+
def warn_master_key_withheld_once
|
|
1100
|
+
return if @master_key_withheld_warning_emitted
|
|
1101
|
+
@master_key_withheld_warning_emitted = true
|
|
1102
|
+
warn "[Parse::LiveQuery] admin connection (use_master_key: true) opened " \
|
|
1103
|
+
"inside Parse.without_master_key: the connect frame carries no master " \
|
|
1104
|
+
"key, so every subscription on this connection is ACL-scoped."
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1048
1107
|
# One-time loud warning that this connection bypasses ACL/CLP for
|
|
1049
1108
|
# every subscription. Master-key LiveQuery is connection-level, so
|
|
1050
1109
|
# this is the only place the risk can be surfaced.
|
|
@@ -1063,6 +1122,17 @@ module Parse
|
|
|
1063
1122
|
# silently disagree with the connection's actual authorization.
|
|
1064
1123
|
# Both are "you think you're scoped (or elevated) but you're not."
|
|
1065
1124
|
def warn_subscription_scope_mismatch(use_master_key, session_token)
|
|
1125
|
+
if @connected_as_admin && Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled? &&
|
|
1126
|
+
!@block_on_admin_warning_emitted
|
|
1127
|
+
# Parse Server fixes master-key authorization per connection at
|
|
1128
|
+
# connect time, so a socket that connected with the master key
|
|
1129
|
+
# stays elevated even for a subscription made inside the block.
|
|
1130
|
+
@block_on_admin_warning_emitted = true
|
|
1131
|
+
warn "[Parse::LiveQuery:SECURITY] subscribe inside Parse.without_master_key " \
|
|
1132
|
+
"on a connection that already connected with the master key: Parse " \
|
|
1133
|
+
"Server authorizes per connection, so this subscription still " \
|
|
1134
|
+
"bypasses ACL/CLP. Use a non-admin client for streams inside the block."
|
|
1135
|
+
end
|
|
1066
1136
|
if use_master_key && !admin_connection?
|
|
1067
1137
|
return if @per_sub_master_key_warning_emitted
|
|
1068
1138
|
@per_sub_master_key_warning_emitted = true
|
data/lib/parse/lock_backend.rb
CHANGED
|
@@ -164,9 +164,10 @@ module Parse
|
|
|
164
164
|
# a holder whose lease expired and was re-acquired by someone else
|
|
165
165
|
# can never delete the new holder's key. Falls back to a
|
|
166
166
|
# best-effort GET-then-DEL for raw-Moneta stores, where the
|
|
167
|
-
# worst-case cross-holder-delete race is bounded by the
|
|
168
|
-
#
|
|
169
|
-
#
|
|
167
|
+
# worst-case cross-holder-delete race is bounded by the lease TTL.
|
|
168
|
+
# `Parse::Lock` clamps `ttl:` to 30s and the `first_or_create!`
|
|
169
|
+
# create-lock to 300s. This is a documented residual risk for the
|
|
170
|
+
# non-Redis path.
|
|
170
171
|
#
|
|
171
172
|
# @param store [Object] Moneta-shaped store.
|
|
172
173
|
# @param key [String] cache key.
|
|
@@ -271,9 +271,10 @@ module Parse
|
|
|
271
271
|
alias_method :delete, :remove
|
|
272
272
|
|
|
273
273
|
# Atomically adds all items to the array field. The request is sent
|
|
274
|
-
# directly to the Parse backend. On success the local collection
|
|
275
|
-
#
|
|
276
|
-
#
|
|
274
|
+
# directly to the Parse backend. On success the local collection takes
|
|
275
|
+
# the array the server returned (or appends the items when the reply
|
|
276
|
+
# does not include the field) without marking the field as changed, so
|
|
277
|
+
# a later save does not overwrite the server array. On
|
|
277
278
|
# an owner that has not been saved yet there is nothing to update on
|
|
278
279
|
# the server, so the items are added locally as a normal change and
|
|
279
280
|
# sent with the next save.
|
|
@@ -509,11 +510,11 @@ module Parse
|
|
|
509
510
|
# Read the owner's dirty state before touching the items, so a change
|
|
510
511
|
# that was already pending is still sent by the next save.
|
|
511
512
|
was_dirty = delegate_field_dirty?
|
|
512
|
-
# A clean
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
#
|
|
516
|
-
#
|
|
513
|
+
# A clean array adopts the array the server returned, so a local copy
|
|
514
|
+
# that was already out of date is corrected. With unsaved local edits
|
|
515
|
+
# pending, adopting it would discard them, so the operation is applied
|
|
516
|
+
# locally instead (as it is for a reply without the field) and the
|
|
517
|
+
# pending edits are still sent on save.
|
|
517
518
|
server = was_dirty ? nil : server_array_after_op
|
|
518
519
|
@collection = server || yield(collection.to_a.dup, items)
|
|
519
520
|
@loaded = true
|
|
@@ -528,13 +529,20 @@ module Parse
|
|
|
528
529
|
end
|
|
529
530
|
|
|
530
531
|
# The field's new array from the delegate's last atomic operation, when
|
|
531
|
-
#
|
|
532
|
+
# the server returned it.
|
|
532
533
|
# @return [Array, nil]
|
|
533
534
|
def server_array_after_op
|
|
534
|
-
return nil unless instance_of?(Parse::CollectionProxy)
|
|
535
535
|
return nil unless @delegate.respond_to?(:_last_operation_value, true)
|
|
536
536
|
value = @delegate.send(:_last_operation_value, @key)
|
|
537
|
-
value.is_a?(Array) ?
|
|
537
|
+
value.is_a?(Array) ? adopt_server_items(value) : nil
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# The local items for an array the server returned, or nil to apply the
|
|
541
|
+
# operation locally instead.
|
|
542
|
+
# @param value [Array] the field's array from the server's reply.
|
|
543
|
+
# @return [Array, nil]
|
|
544
|
+
def adopt_server_items(value)
|
|
545
|
+
Parse::Properties.deep_copy_value(value)
|
|
538
546
|
end
|
|
539
547
|
|
|
540
548
|
# Convert items to pointer format for atomic operations.
|
|
@@ -555,6 +555,17 @@ module Parse
|
|
|
555
555
|
warn "[#{self.class}] has_many :#{key} expected className=#{klassName.inspect}, ignoring incoming className=#{val[Parse::Model::KEY_CLASS_NAME].inspect}"
|
|
556
556
|
end
|
|
557
557
|
if val.is_a?(Hash) && val["__type"] == "Relation"
|
|
558
|
+
current = instance_variable_get(ivar)
|
|
559
|
+
# A server descriptor carries no membership. When the current
|
|
560
|
+
# proxy holds staged additions or removals (a fetch of a record
|
|
561
|
+
# with unsaved relation changes), keep it so they are not lost.
|
|
562
|
+
if track != true && current.is_a?(Parse::RelationCollectionProxy) &&
|
|
563
|
+
current.staged_changes?
|
|
564
|
+
# The loaded list predates this fetch. Unload it so the next
|
|
565
|
+
# read queries the server and re-applies the staged changes.
|
|
566
|
+
current.reset! if current.loaded?
|
|
567
|
+
return current
|
|
568
|
+
end
|
|
558
569
|
relation_objects = val["objects"] || []
|
|
559
570
|
val = Parse::RelationCollectionProxy.new relation_objects, delegate: self, key: key, parse_class: klassName
|
|
560
571
|
elsif val.is_a?(Hash) && val["__op"] == "AddRelation" && val["objects"].present?
|
|
@@ -190,6 +190,42 @@ module Parse
|
|
|
190
190
|
end
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# The server returns the field's array as pointer hashes. Each entry
|
|
194
|
+
# becomes an object of the declared class, reusing the local object with
|
|
195
|
+
# the same id so fetched data is kept. The array is adopted only when
|
|
196
|
+
# the declared class is a registered model and every entry is a pointer
|
|
197
|
+
# to that class; anything else (another class, a bare string, a value
|
|
198
|
+
# that is not a pointer) makes the operation apply locally instead,
|
|
199
|
+
# rather than relabeling the entry as the declared class.
|
|
200
|
+
# @return [Array<Parse::Pointer>, nil]
|
|
201
|
+
def adopt_server_items(value)
|
|
202
|
+
return nil if @parse_class.blank? || Parse::Model.find_class(@parse_class).nil?
|
|
203
|
+
local_by_id = {}
|
|
204
|
+
collection.to_a.each do |item|
|
|
205
|
+
id = item.respond_to?(:id) ? item.id : nil
|
|
206
|
+
local_by_id[id] ||= item if id.present?
|
|
207
|
+
end
|
|
208
|
+
value.map do |entry|
|
|
209
|
+
class_name, object_id = server_pointer_parts(entry)
|
|
210
|
+
return nil unless object_id.is_a?(String) && object_id.present?
|
|
211
|
+
return nil unless Parse::Model.same_parse_class?(class_name, @parse_class)
|
|
212
|
+
local_by_id[object_id] || typecast_item(entry) || (return nil)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @return [Array(String, String), nil] the className and objectId of a
|
|
217
|
+
# pointer entry from a server reply, or nil when it is not a pointer.
|
|
218
|
+
def server_pointer_parts(entry)
|
|
219
|
+
case entry
|
|
220
|
+
when Parse::Pointer
|
|
221
|
+
[entry.parse_class, entry.id]
|
|
222
|
+
when Hash
|
|
223
|
+
type = entry["__type"] || entry[:__type]
|
|
224
|
+
return nil unless %w[Pointer Object].include?(type.to_s)
|
|
225
|
+
[entry["className"] || entry[:className], entry["objectId"] || entry[:objectId]]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
193
229
|
# @return [Parse::Pointer, nil] the item as a Parse object, or nil.
|
|
194
230
|
def typecast_item(item)
|
|
195
231
|
case item
|
|
@@ -24,18 +24,19 @@ module Parse
|
|
|
24
24
|
# get matching items within the relation collection.
|
|
25
25
|
#
|
|
26
26
|
# When creating a Relation proxy, all the delegate methods defined in the superclasses
|
|
27
|
-
# need to be implemented, in addition to
|
|
28
|
-
# _relation_query and _commit_relation_updates . :'key'_relation_query should return a
|
|
27
|
+
# need to be implemented, in addition to :'key'_relation_query, which should return a
|
|
29
28
|
# Parse::Query object that is properly tied to the foreign table class related to this object column.
|
|
30
29
|
# Example, if an Artist has many Song objects, then the query to be returned by this method
|
|
31
30
|
# should be a Parse::Query for the class 'Song'.
|
|
32
31
|
# Because relation changes are separate from object changes, you can call save on a
|
|
33
|
-
# relation collection to
|
|
34
|
-
#
|
|
35
|
-
# through :'key'_commit_relation_updates message. The delegate is also in charge of
|
|
36
|
-
# clearing out the change information for the collection if saved successfully.
|
|
32
|
+
# relation collection to send only its staged add and remove operations, through the
|
|
33
|
+
# owner's atomic relation operations.
|
|
37
34
|
# @see PointerCollectionProxy
|
|
38
35
|
class RelationCollectionProxy < PointerCollectionProxy
|
|
36
|
+
# @!visibility private
|
|
37
|
+
# Default for {#save}'s `session:`: keep the owner's session as it is.
|
|
38
|
+
SESSION_UNSET = Object.new.freeze
|
|
39
|
+
|
|
39
40
|
define_attribute_methods :additions, :removals
|
|
40
41
|
# @!attribute [r] removals
|
|
41
42
|
# The objects that have been newly removed to this collection
|
|
@@ -200,7 +201,7 @@ module Parse
|
|
|
200
201
|
return false unless @delegate.respond_to?(:op_add_relation!)
|
|
201
202
|
items = typecast_items(items)
|
|
202
203
|
return true if items.empty?
|
|
203
|
-
return add(*items) && true unless
|
|
204
|
+
return add(*items) && true unless owner_persisted?
|
|
204
205
|
return false unless @delegate.send(:op_add_relation!, @key, items.parse_pointers)
|
|
205
206
|
items.each do |item|
|
|
206
207
|
@removals.delete(item)
|
|
@@ -223,7 +224,7 @@ module Parse
|
|
|
223
224
|
return false unless @delegate.respond_to?(:op_remove_relation!)
|
|
224
225
|
items = typecast_items(items, strict: false)
|
|
225
226
|
return true if items.empty?
|
|
226
|
-
return remove(*items) && true unless
|
|
227
|
+
return remove(*items) && true unless owner_persisted?
|
|
227
228
|
return false unless @delegate.send(:op_remove_relation!, @key, items.parse_pointers)
|
|
228
229
|
items.each do |item|
|
|
229
230
|
@additions.delete(item)
|
|
@@ -251,11 +252,66 @@ module Parse
|
|
|
251
252
|
@collection
|
|
252
253
|
end
|
|
253
254
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
# @return [Boolean] true when additions or removals are staged and not
|
|
256
|
+
# yet sent.
|
|
257
|
+
def staged_changes?
|
|
258
|
+
@additions.any? || @removals.any?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Drops the staged additions and removals and clears the dirty tracking,
|
|
262
|
+
# without sending anything. When operations were staged, the items are
|
|
263
|
+
# reloaded from the server on the next access, since the loaded list
|
|
264
|
+
# included them.
|
|
265
|
+
def clear_changes!
|
|
266
|
+
staged = staged_changes?
|
|
267
|
+
@additions = []
|
|
268
|
+
@removals = []
|
|
269
|
+
super
|
|
270
|
+
reset! if staged
|
|
271
|
+
@collection
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Sends the staged additions and removals of this relation without saving
|
|
275
|
+
# the rest of the owner. The owner must already be saved: an unsaved owner
|
|
276
|
+
# sends its relation additions when it is created.
|
|
277
|
+
#
|
|
278
|
+
# Removals and additions go out as two requests, removals first (each
|
|
279
|
+
# through the owner's atomic relation operation, so {Parse::Role} cache
|
|
280
|
+
# invalidation still runs). The two are not atomic: if the removals
|
|
281
|
+
# succeed and the additions fail, the removals stay applied, they are no
|
|
282
|
+
# longer staged, and the additions stay staged for a retry. An owner
|
|
283
|
+
# save sends relation changes the same way.
|
|
284
|
+
#
|
|
285
|
+
# The requests use the owner's session token from its last
|
|
286
|
+
# `save(session:)`, or the client's default credentials. Pass `session:`
|
|
287
|
+
# to send them as a specific user (a session token String or a
|
|
288
|
+
# {Parse::User}), or `session: nil` to send them with no session token.
|
|
289
|
+
# @param session [String, Parse::User, nil] the session to send the
|
|
290
|
+
# operations with, when given.
|
|
291
|
+
# @return [Boolean] whether the staged operations were applied. False
|
|
292
|
+
# when the owner is not saved yet or a request failed; operations that
|
|
293
|
+
# did not succeed stay staged.
|
|
294
|
+
def save(session: SESSION_UNSET)
|
|
295
|
+
return true if @additions.empty? && @removals.empty?
|
|
296
|
+
return false unless owner_persisted?
|
|
297
|
+
return false unless @delegate.respond_to?(:op_add_relation!) && @delegate.respond_to?(:op_remove_relation!)
|
|
298
|
+
applied = with_owner_session(session) do
|
|
299
|
+
if @removals.any?
|
|
300
|
+
next false unless @delegate.send(:op_remove_relation!, @key, @removals.parse_pointers)
|
|
301
|
+
@removals = []
|
|
302
|
+
end
|
|
303
|
+
if @additions.any?
|
|
304
|
+
next false unless @delegate.send(:op_add_relation!, @key, @additions.parse_pointers)
|
|
305
|
+
@additions = []
|
|
306
|
+
end
|
|
307
|
+
true
|
|
308
|
+
end
|
|
309
|
+
return false unless applied
|
|
310
|
+
clear_changes_information
|
|
311
|
+
if @delegate.respond_to?(:clear_attribute_changes, true)
|
|
312
|
+
@delegate.send(:clear_attribute_changes, [@key.to_s])
|
|
258
313
|
end
|
|
314
|
+
true
|
|
259
315
|
end
|
|
260
316
|
|
|
261
317
|
# @see #add
|
|
@@ -273,6 +329,35 @@ module Parse
|
|
|
273
329
|
!(@delegate.respond_to?(:id) && @delegate.id.blank?)
|
|
274
330
|
end
|
|
275
331
|
|
|
332
|
+
# @return [Boolean] whether the owner exists on the server, so a write to
|
|
333
|
+
# its relation can be sent. Follows {Parse::Object#persisted?}: an
|
|
334
|
+
# id-only handle to a stored record counts, but an objectId assigned
|
|
335
|
+
# client-side during a create (`parse_reference precompute:`) does not
|
|
336
|
+
# until the create returns, and neither does a destroyed owner.
|
|
337
|
+
def owner_persisted?
|
|
338
|
+
return false unless owner_saved?
|
|
339
|
+
!@delegate.respond_to?(:persisted?) || @delegate.persisted?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Run the block with the owner's session token set to `session`, then
|
|
343
|
+
# restore the owner's previous token. {SESSION_UNSET} runs it unchanged.
|
|
344
|
+
def with_owner_session(session)
|
|
345
|
+
return yield if session.equal?(SESSION_UNSET)
|
|
346
|
+
token = @delegate.send(:_validate_session_token!, session, :save)
|
|
347
|
+
had = @delegate.instance_variable_defined?(:@_session_token)
|
|
348
|
+
previous = @delegate.instance_variable_get(:@_session_token)
|
|
349
|
+
@delegate.instance_variable_set(:@_session_token, token)
|
|
350
|
+
begin
|
|
351
|
+
yield
|
|
352
|
+
ensure
|
|
353
|
+
if had
|
|
354
|
+
@delegate.instance_variable_set(:@_session_token, previous)
|
|
355
|
+
else
|
|
356
|
+
@delegate.remove_instance_variable(:@_session_token)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
276
361
|
# ActiveModel reads the current value when a change starts. Read the
|
|
277
362
|
# items directly: going through {#collection} would query the whole
|
|
278
363
|
# relation just to stage an add or remove.
|
|
@@ -279,6 +279,12 @@ module Parse
|
|
|
279
279
|
# query/constraints) — none of those have a caller scope to
|
|
280
280
|
# forward. The fast path is opt-in for performance-conscious
|
|
281
281
|
# callers that can supply explicit authorization.
|
|
282
|
+
# @note Inside {Parse.without_master_key}, the Parse Server walk (no
|
|
283
|
+
# `master:` or `as:`) still reads `_Role` with the master key as SDK
|
|
284
|
+
# metadata, so it returns the full role-name closure of `user`, not
|
|
285
|
+
# only the publicly readable roles. The block guards against
|
|
286
|
+
# accidental master-key use; it does not scope this lookup. Pass
|
|
287
|
+
# `as:` for a scope-checked answer.
|
|
282
288
|
# @example
|
|
283
289
|
# names = Parse::Role.all_for_user(user, master: true) # admin/analytics
|
|
284
290
|
# names = Parse::Role.all_for_user(user, as: current_user) # scope-checked
|
|
@@ -423,6 +429,20 @@ module Parse
|
|
|
423
429
|
end
|
|
424
430
|
|
|
425
431
|
def role_query_all(constraints, client: nil)
|
|
432
|
+
# Inside `Parse.without_master_key` the role graph is still read with
|
|
433
|
+
# the master key. It is metadata the SDK needs to enforce a scope, as
|
|
434
|
+
# Parse Server reads it itself; read without the key it returned only
|
|
435
|
+
# publicly readable roles, and that short closure was then cached for
|
|
436
|
+
# the user and served to every caller until it expired.
|
|
437
|
+
if Parse.respond_to?(:master_key_disabled?) && Parse.master_key_disabled?
|
|
438
|
+
query = Parse::Role.query(constraints.reverse_merge(limit: :max))
|
|
439
|
+
query.client = client if client
|
|
440
|
+
# A master-key read: keep it out of the shared response cache.
|
|
441
|
+
query.cache = false
|
|
442
|
+
query.instance_variable_set(:@_metadata_master, true)
|
|
443
|
+
return query.results
|
|
444
|
+
end
|
|
445
|
+
|
|
426
446
|
# No explicit client means the historical path, unchanged. This is not
|
|
427
447
|
# only for compatibility: `Parse::Role.all` is what callers and tests
|
|
428
448
|
# observe and stub, and routing around it when nothing asked us to
|
|
@@ -957,6 +977,9 @@ module Parse
|
|
|
957
977
|
# another's role names.
|
|
958
978
|
# @param strict [Boolean] re-raise role-query failures rather than returning
|
|
959
979
|
# a partial parent closure.
|
|
980
|
+
# @note Inside {Parse.without_master_key} the walk still reads `_Role`
|
|
981
|
+
# with the master key as SDK metadata, so it returns the full parent
|
|
982
|
+
# closure, not only publicly readable roles.
|
|
960
983
|
def all_parent_role_names(max_depth: 10, client: nil, strict: false)
|
|
961
984
|
Parse::Role.expand_inheritance_upward(
|
|
962
985
|
[self], max_depth: max_depth, client: client, strict: strict,
|