parse-stack-next 5.7.6 → 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 +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
data/lib/parse/model/bytes.rb
CHANGED
|
@@ -34,10 +34,12 @@ module Parse
|
|
|
34
34
|
ATTRIBUTES
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Base64 encode and set the instance contents
|
|
37
|
+
# Base64 encode and set the instance contents. Uses strict (RFC 4648)
|
|
38
|
+
# encoding: `Base64.encode64` inserts a newline every 60 characters,
|
|
39
|
+
# which Parse Server stores verbatim in the Bytes column.
|
|
38
40
|
# @param str the string to encode
|
|
39
41
|
def encode(str)
|
|
40
|
-
@base64 = Base64.
|
|
42
|
+
@base64 = Base64.strict_encode64(str.to_s)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Get the content as decoded base64 bytes
|
|
@@ -45,11 +47,17 @@ module Parse
|
|
|
45
47
|
Base64.decode64(@base64 || "")
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
# Mass-assign the base64 content from a base64 String, a Parse Bytes
|
|
51
|
+
# hash (`{"__type" => "Bytes", "base64" => "..."}`), or another
|
|
52
|
+
# {Parse::Bytes}.
|
|
53
|
+
# @param a [String, Hash, Parse::Bytes]
|
|
48
54
|
def attributes=(a)
|
|
49
|
-
if a.is_a?(
|
|
50
|
-
@
|
|
55
|
+
if a.is_a?(Bytes)
|
|
56
|
+
@base64 = a.base64.dup
|
|
57
|
+
elsif a.is_a?(String)
|
|
58
|
+
@base64 = a.dup
|
|
51
59
|
elsif a.is_a?(Hash)
|
|
52
|
-
@
|
|
60
|
+
@base64 = a["base64"] || a[:base64] || @base64
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
|
|
@@ -62,6 +62,78 @@ module Parse
|
|
|
62
62
|
# @return [RelationCollectionProxy<User>] a Parse relation of users belonging to this role.
|
|
63
63
|
has_many :users, through: :relation
|
|
64
64
|
|
|
65
|
+
# Grants and revocations reach mongo-direct ACL resolution immediately
|
|
66
|
+
# rather than after the role-cache TTL: the users whose membership a save
|
|
67
|
+
# changes have their cached role closures dropped, and a change to the
|
|
68
|
+
# role hierarchy drops them all.
|
|
69
|
+
before_save :_capture_role_membership_changes
|
|
70
|
+
after_save :_invalidate_role_caches
|
|
71
|
+
|
|
72
|
+
# Atomic relation operations (`role.users.add!`, `role.roles.remove!`)
|
|
73
|
+
# write immediately without a save, so they invalidate the same caches.
|
|
74
|
+
# @!visibility private
|
|
75
|
+
def op_add_relation!(field, objects = [])
|
|
76
|
+
result = super
|
|
77
|
+
_invalidate_after_relation_op(field, objects) if result
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @!visibility private
|
|
82
|
+
def op_remove_relation!(field, objects = [])
|
|
83
|
+
result = super
|
|
84
|
+
_invalidate_after_relation_op(field, objects) if result
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @!visibility private
|
|
89
|
+
def _invalidate_after_relation_op(field, objects)
|
|
90
|
+
key = field.to_s
|
|
91
|
+
if %w[roles].include?(key)
|
|
92
|
+
@_role_hierarchy_changed = true
|
|
93
|
+
elsif %w[users].include?(key)
|
|
94
|
+
@_role_membership_changed_ids = Array(objects).filter_map do |u|
|
|
95
|
+
id = u.respond_to?(:id) ? u.id : (u.is_a?(Hash) ? (u["objectId"] || u[:objectId]) : u)
|
|
96
|
+
id.to_s if id.present?
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
_invalidate_role_caches
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @!visibility private
|
|
105
|
+
def _capture_role_membership_changes
|
|
106
|
+
users_proxy = instance_variable_get(:@users)
|
|
107
|
+
ids = []
|
|
108
|
+
if users_proxy.respond_to?(:additions)
|
|
109
|
+
(Array(users_proxy.additions) + Array(users_proxy.removals)).each do |u|
|
|
110
|
+
id = u.respond_to?(:id) ? u.id : u
|
|
111
|
+
ids << id.to_s if id.present?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
roles_proxy = instance_variable_get(:@roles)
|
|
115
|
+
@_role_hierarchy_changed = roles_proxy.respond_to?(:additions) &&
|
|
116
|
+
(Array(roles_proxy.additions).any? || Array(roles_proxy.removals).any?)
|
|
117
|
+
@_role_membership_changed_ids = ids.uniq
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @!visibility private
|
|
122
|
+
def _invalidate_role_caches
|
|
123
|
+
auth = client.respond_to?(:authorization) ? client.authorization : nil
|
|
124
|
+
return unless auth
|
|
125
|
+
if @_role_hierarchy_changed && auth.respond_to?(:invalidate_all_roles)
|
|
126
|
+
auth.invalidate_all_roles
|
|
127
|
+
elsif auth.respond_to?(:invalidate_user_roles)
|
|
128
|
+
Array(@_role_membership_changed_ids).each { |id| auth.invalidate_user_roles(id) }
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
warn "[Parse::Role] role cache invalidation failed: #{e.class}"
|
|
132
|
+
ensure
|
|
133
|
+
@_role_membership_changed_ids = nil
|
|
134
|
+
@_role_hierarchy_changed = nil
|
|
135
|
+
end
|
|
136
|
+
|
|
65
137
|
# Names of Mongo driver errors that mean "the server is momentarily
|
|
66
138
|
# unreachable", for which falling back to the Parse Server walk is right.
|
|
67
139
|
#
|
|
@@ -207,6 +279,12 @@ module Parse
|
|
|
207
279
|
# query/constraints) — none of those have a caller scope to
|
|
208
280
|
# forward. The fast path is opt-in for performance-conscious
|
|
209
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.
|
|
210
288
|
# @example
|
|
211
289
|
# names = Parse::Role.all_for_user(user, master: true) # admin/analytics
|
|
212
290
|
# names = Parse::Role.all_for_user(user, as: current_user) # scope-checked
|
|
@@ -351,6 +429,20 @@ module Parse
|
|
|
351
429
|
end
|
|
352
430
|
|
|
353
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
|
+
|
|
354
446
|
# No explicit client means the historical path, unchanged. This is not
|
|
355
447
|
# only for compatibility: `Parse::Role.all` is what callers and tests
|
|
356
448
|
# observe and stub, and routing around it when nothing asked us to
|
|
@@ -885,6 +977,9 @@ module Parse
|
|
|
885
977
|
# another's role names.
|
|
886
978
|
# @param strict [Boolean] re-raise role-query failures rather than returning
|
|
887
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.
|
|
888
983
|
def all_parent_role_names(max_depth: 10, client: nil, strict: false)
|
|
889
984
|
Parse::Role.expand_inheritance_upward(
|
|
890
985
|
[self], max_depth: max_depth, client: client, strict: strict,
|
|
@@ -90,7 +90,9 @@ module Parse
|
|
|
90
90
|
opts.delete(:session_token)
|
|
91
91
|
response = client.fetch_session(token, **opts)
|
|
92
92
|
if response.success?
|
|
93
|
-
|
|
93
|
+
built = Parse::Session.build response.result
|
|
94
|
+
_remember_owners!([built], client)
|
|
95
|
+
return built
|
|
94
96
|
end
|
|
95
97
|
nil
|
|
96
98
|
end
|
|
@@ -204,5 +206,306 @@ module Parse
|
|
|
204
206
|
def revoke!
|
|
205
207
|
destroy
|
|
206
208
|
end
|
|
209
|
+
|
|
210
|
+
# Deletes the session and forgets it in the client's identity plane, so
|
|
211
|
+
# mongo-direct and Atlas Search reads stop resolving the token at once
|
|
212
|
+
# instead of when the cached entry expires. Both the token and the owning
|
|
213
|
+
# user's entries are dropped. A session that does not carry them (built
|
|
214
|
+
# from its objectId, or fetched without the master key or with `keys:`
|
|
215
|
+
# leaving them out) has them looked up first: with the master key as SDK
|
|
216
|
+
# metadata when the client has one, otherwise with the `session:` passed
|
|
217
|
+
# here. A lookup that finds no row leaves nothing to forget. Only a
|
|
218
|
+
# lookup that fails outright drops every cached identity on the client,
|
|
219
|
+
# at most once every {RESET_INTERVAL} seconds per client.
|
|
220
|
+
#
|
|
221
|
+
# The entries are dropped when the delete succeeds or reports the row
|
|
222
|
+
# is already gone (or not visible to the caller). A delete that raises
|
|
223
|
+
# still drops the known token, which is harmless, but does not touch the
|
|
224
|
+
# owner's entries.
|
|
225
|
+
#
|
|
226
|
+
# A batch delete through `Array#destroy` drops the same entries for
|
|
227
|
+
# each session in it. With an identity plane that cannot drop a
|
|
228
|
+
# single user's entries (a custom plane without `bump_generation` or
|
|
229
|
+
# `invalidate_value`), a revoked token keeps resolving until its cached
|
|
230
|
+
# entry expires.
|
|
231
|
+
# @param session [String] (see Parse::Object#destroy)
|
|
232
|
+
# @return [Boolean] whether the operation was successful.
|
|
233
|
+
def destroy(session: nil)
|
|
234
|
+
# A session never saved is not deleted (see Parse::Object#destroy), so
|
|
235
|
+
# there is nothing to forget either.
|
|
236
|
+
return super if new?
|
|
237
|
+
token = nil
|
|
238
|
+
owner_id = nil
|
|
239
|
+
begin
|
|
240
|
+
Parse::Session.send(:_preload_identity_for_destroy!, [self], session_token: session)
|
|
241
|
+
token, owner_id = _identity_for_destroy
|
|
242
|
+
ensure
|
|
243
|
+
# The looked-up token is a live credential: keep it in locals only.
|
|
244
|
+
_clear_identity_for_destroy!
|
|
245
|
+
end
|
|
246
|
+
result = nil
|
|
247
|
+
begin
|
|
248
|
+
result = super
|
|
249
|
+
ensure
|
|
250
|
+
# `false` covers "object not found", which is either a session
|
|
251
|
+
# already revoked elsewhere or one the caller cannot see; dropping
|
|
252
|
+
# cached entries is idempotent in both cases. A raised delete still
|
|
253
|
+
# drops the token it named but leaves the owner alone. A single
|
|
254
|
+
# delete cannot tell "already gone" from "denied", so an absent row
|
|
255
|
+
# only uses the recorded owner, never the reset fallback.
|
|
256
|
+
if result.nil?
|
|
257
|
+
_forget_identity!(token.is_a?(String) ? token : nil, nil)
|
|
258
|
+
else
|
|
259
|
+
_forget_identity!(token, owner_id, reset_fallback: false)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Seconds between two full identity-cache resets triggered by a failed
|
|
265
|
+
# session lookup on the same client.
|
|
266
|
+
RESET_INTERVAL = 5
|
|
267
|
+
|
|
268
|
+
# Process-local record of the last lookup-triggered reset per client.
|
|
269
|
+
@identity_reset_at = {}
|
|
270
|
+
@identity_reset_mutex = Mutex.new
|
|
271
|
+
|
|
272
|
+
class << self
|
|
273
|
+
# Record, in the identity context of `cl` (the client that fetched
|
|
274
|
+
# them), which user owns each session that carries both its objectId
|
|
275
|
+
# and its owner, so a later delete can drop the owner's cached
|
|
276
|
+
# identities after the row is gone. Never raises: it runs while query
|
|
277
|
+
# results are decoded.
|
|
278
|
+
# @param sessions [Array<Parse::Object>]
|
|
279
|
+
# @param cl [Parse::Client]
|
|
280
|
+
# @!visibility private
|
|
281
|
+
def _remember_owners!(sessions, cl)
|
|
282
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
283
|
+
return unless auth.respond_to?(:remember_session_owner)
|
|
284
|
+
Array(sessions).each do |o|
|
|
285
|
+
next unless o.is_a?(Parse::Session)
|
|
286
|
+
owner_id = o.send(:_identity_owner_id)
|
|
287
|
+
id = o.id
|
|
288
|
+
auth.remember_session_owner(id, owner_id) if owner_id && id.present?
|
|
289
|
+
end
|
|
290
|
+
nil
|
|
291
|
+
rescue StandardError
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
# Reset `cl`'s identity and role caches, at most once every
|
|
298
|
+
# {RESET_INTERVAL} seconds per client.
|
|
299
|
+
# @!visibility private
|
|
300
|
+
def _rate_limited_identity_reset!(cl)
|
|
301
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
302
|
+
return unless auth.respond_to?(:reset_caches!)
|
|
303
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
304
|
+
due = @identity_reset_mutex.synchronize do
|
|
305
|
+
last = @identity_reset_at[cl.object_id]
|
|
306
|
+
next false if last && now - last < RESET_INTERVAL
|
|
307
|
+
@identity_reset_at[cl.object_id] = now
|
|
308
|
+
true
|
|
309
|
+
end
|
|
310
|
+
auth.reset_caches! if due
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Look up the token and owner of every session about to be deleted that
|
|
314
|
+
# does not carry them, so the delete can drop their identity entries.
|
|
315
|
+
# One `_Session` query per client. A client with a master key reads it
|
|
316
|
+
# as SDK metadata (it works inside `Parse.without_master_key`); one
|
|
317
|
+
# without reads it with `session_token`, or skips the lookup when there
|
|
318
|
+
# is none. The query never uses the response cache: its rows carry live
|
|
319
|
+
# session tokens.
|
|
320
|
+
#
|
|
321
|
+
# A row the lookup did not return is marked absent: there is nothing
|
|
322
|
+
# to forget for it. Only a lookup that raised marks its sessions
|
|
323
|
+
# `:unknown`, which makes their delete reset the client's identity
|
|
324
|
+
# cache (rate limited).
|
|
325
|
+
# @param sessions [Array<Parse::Object>]
|
|
326
|
+
# @param session_token [String, nil] the delete's own session.
|
|
327
|
+
# @!visibility private
|
|
328
|
+
def _preload_identity_for_destroy!(sessions, session_token: nil)
|
|
329
|
+
pending = sessions.select { |o| o.is_a?(Parse::Session) && o.send(:_identity_lookup_needed?) }
|
|
330
|
+
return if pending.empty?
|
|
331
|
+
pending.group_by(&:client).each do |cl, group|
|
|
332
|
+
has_master = cl.respond_to?(:master_key) && cl.master_key.present?
|
|
333
|
+
token = session_token.respond_to?(:session_token) ? session_token.session_token : session_token
|
|
334
|
+
token = nil unless token.is_a?(String) && !token.strip.empty?
|
|
335
|
+
unless has_master || token
|
|
336
|
+
group.each { |o| o.instance_variable_set(:@_identity_for_destroy, :absent) }
|
|
337
|
+
next
|
|
338
|
+
end
|
|
339
|
+
ids = group.map(&:id).uniq
|
|
340
|
+
found = begin
|
|
341
|
+
query = Parse::Session.query(:objectId.in => ids, limit: ids.size)
|
|
342
|
+
query.keys(:session_token, :user)
|
|
343
|
+
query.client = cl
|
|
344
|
+
query.cache = false
|
|
345
|
+
if has_master
|
|
346
|
+
query.instance_variable_set(:@_metadata_master, true)
|
|
347
|
+
else
|
|
348
|
+
query.session_token = token
|
|
349
|
+
end
|
|
350
|
+
query.results.to_h do |row|
|
|
351
|
+
owner = row.instance_variable_get(:@user)
|
|
352
|
+
[row.id, [row.instance_variable_get(:@session_token), owner.respond_to?(:id) ? owner.id : nil]]
|
|
353
|
+
end
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
warn "[Parse::Session] could not look up the token and owner of " \
|
|
356
|
+
"#{ids.size} session(s) before deleting them (#{e.class}); " \
|
|
357
|
+
"dropping the whole identity cache instead."
|
|
358
|
+
nil
|
|
359
|
+
end
|
|
360
|
+
group.each do |o|
|
|
361
|
+
mark = if found.nil?
|
|
362
|
+
:unknown
|
|
363
|
+
elsif (row = found[o.id]) && (row[0].is_a?(String) || row[1])
|
|
364
|
+
row
|
|
365
|
+
else
|
|
366
|
+
:absent
|
|
367
|
+
end
|
|
368
|
+
o.instance_variable_set(:@_identity_for_destroy, mark)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Called by `Array#destroy` for each session in the batch with its
|
|
375
|
+
# response. The entries are dropped only when the delete succeeded or
|
|
376
|
+
# reported the row gone ("object not found").
|
|
377
|
+
# @!visibility private
|
|
378
|
+
def _after_batch_destroy(response = nil)
|
|
379
|
+
identity = _identity_for_destroy
|
|
380
|
+
_clear_identity_for_destroy!
|
|
381
|
+
return unless Parse::Session.send(:_destroy_applied?, response)
|
|
382
|
+
# The batch response tells success and "object not found" apart from
|
|
383
|
+
# a denial, so an absent row whose owner was never recorded may fall
|
|
384
|
+
# back to the rate-limited reset here.
|
|
385
|
+
_forget_identity!(*identity, reset_fallback: true)
|
|
386
|
+
end
|
|
387
|
+
private :_after_batch_destroy
|
|
388
|
+
|
|
389
|
+
# Whether a batch delete response means the row is gone: success, or
|
|
390
|
+
# "object not found". A missing response (older callers) counts as
|
|
391
|
+
# applied.
|
|
392
|
+
# @!visibility private
|
|
393
|
+
def self._destroy_applied?(response)
|
|
394
|
+
return true if response.nil?
|
|
395
|
+
(response.respond_to?(:success?) && response.success?) ||
|
|
396
|
+
(response.respond_to?(:object_not_found?) && response.object_not_found?)
|
|
397
|
+
end
|
|
398
|
+
private_class_method :_destroy_applied?
|
|
399
|
+
|
|
400
|
+
# Remove the token and owner {._preload_identity_for_destroy!} looked
|
|
401
|
+
# up. The token is a live credential and must not outlive the delete
|
|
402
|
+
# (it would show in `inspect` or `instance_variables`).
|
|
403
|
+
# @!visibility private
|
|
404
|
+
def _clear_identity_for_destroy!
|
|
405
|
+
remove_instance_variable(:@_identity_for_destroy) if instance_variable_defined?(:@_identity_for_destroy)
|
|
406
|
+
end
|
|
407
|
+
private :_clear_identity_for_destroy!
|
|
408
|
+
|
|
409
|
+
# Whether this session lacks the token or the owner its identity-plane
|
|
410
|
+
# entries are keyed by: a fetch without the master key (no token) or
|
|
411
|
+
# with `keys:` leaving them out.
|
|
412
|
+
# @!visibility private
|
|
413
|
+
def _identity_lookup_needed?
|
|
414
|
+
# Keyed on the objectId, not `new?`: `Array#destroy` deletes by id
|
|
415
|
+
# alone, so a session built from an id (no timestamps loaded) is still
|
|
416
|
+
# deleted and its token must still be dropped.
|
|
417
|
+
return false if @id.blank?
|
|
418
|
+
!@session_token.is_a?(String) || @session_token.empty? || _identity_owner_id.nil?
|
|
419
|
+
end
|
|
420
|
+
private :_identity_lookup_needed?
|
|
421
|
+
|
|
422
|
+
# Serialization omits `sessionToken` unless `include_session_token: true`
|
|
423
|
+
# is passed. A session token is a bearer credential, and `as_json` is the
|
|
424
|
+
# surface that reaches logs, API responses and agent tool output.
|
|
425
|
+
# @param opts [Hash] see Parse::Object#as_json.
|
|
426
|
+
# @option opts [Boolean] :include_session_token include the token.
|
|
427
|
+
# @return [Hash]
|
|
428
|
+
def as_json(opts = nil)
|
|
429
|
+
opts = (opts || {}).dup
|
|
430
|
+
include_token = opts.delete(:include_session_token) == true
|
|
431
|
+
json = super(opts)
|
|
432
|
+
return json if include_token || !json.is_a?(Hash)
|
|
433
|
+
json.except("session_token", "sessionToken", :session_token, :sessionToken)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Redacts the session token from the default inspect output.
|
|
437
|
+
# @return [String]
|
|
438
|
+
def inspect
|
|
439
|
+
looked_up = @_identity_for_destroy
|
|
440
|
+
Parse::User.redact_session_token(super, @session_token, looked_up.is_a?(Array) ? looked_up[0] : nil)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private
|
|
444
|
+
|
|
445
|
+
# The token and owner id to forget for this delete: what this instance
|
|
446
|
+
# carries, completed by {._preload_identity_for_destroy!} when it did
|
|
447
|
+
# not, or `:unknown` when that lookup failed.
|
|
448
|
+
# @!visibility private
|
|
449
|
+
def _identity_for_destroy
|
|
450
|
+
looked_up = @_identity_for_destroy
|
|
451
|
+
token = @session_token.is_a?(String) && !@session_token.empty? ? @session_token : nil
|
|
452
|
+
owner_id = _identity_owner_id
|
|
453
|
+
return [:unknown, owner_id] if looked_up == :unknown
|
|
454
|
+
if looked_up == :absent
|
|
455
|
+
# No row to read: the session is gone or not visible. Forget what
|
|
456
|
+
# this instance carries; with nothing at all, mark it absent so the
|
|
457
|
+
# delete can fall back to the recorded owner.
|
|
458
|
+
return token || owner_id ? [token, owner_id] : [:absent, nil]
|
|
459
|
+
end
|
|
460
|
+
if looked_up.is_a?(Array)
|
|
461
|
+
token ||= looked_up[0]
|
|
462
|
+
owner_id ||= looked_up[1]
|
|
463
|
+
end
|
|
464
|
+
[token, owner_id]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# The owning user's id, read from the association ivar directly:
|
|
468
|
+
# calling `user` on a partially fetched session could trigger an
|
|
469
|
+
# autofetch just to learn the owner.
|
|
470
|
+
# @!visibility private
|
|
471
|
+
def _identity_owner_id
|
|
472
|
+
owner = @user
|
|
473
|
+
owner.respond_to?(:id) ? owner.id : nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Drop the token and the owner's entries from this session's client's
|
|
477
|
+
# identity plane.
|
|
478
|
+
# @!visibility private
|
|
479
|
+
def _forget_identity!(token, owner_id, reset_fallback: false)
|
|
480
|
+
cl = client
|
|
481
|
+
if token == :absent
|
|
482
|
+
# The row was not readable before the delete. Use the owner recorded
|
|
483
|
+
# when this session was last loaded; without one, and only when the
|
|
484
|
+
# delete is known to have applied or found the row gone, fall back to
|
|
485
|
+
# the rate-limited reset. Never on a denied delete.
|
|
486
|
+
token = nil
|
|
487
|
+
auth = cl.respond_to?(:authorization) ? cl.authorization : nil
|
|
488
|
+
recorded = auth.respond_to?(:session_owner) ? auth.session_owner(@id) : nil
|
|
489
|
+
if recorded
|
|
490
|
+
owner_id ||= recorded
|
|
491
|
+
elsif owner_id.nil? && reset_fallback
|
|
492
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
if token == :unknown
|
|
496
|
+
# The lookup failed, so the token's entry cannot be named. Drop every
|
|
497
|
+
# cached identity on this client (the next read of each token
|
|
498
|
+
# re-resolves it, which is safe), at most once every RESET_INTERVAL
|
|
499
|
+
# seconds so repeated failures cannot flush a shared plane on every
|
|
500
|
+
# request. The owner's entries go as well when the owner is known.
|
|
501
|
+
Parse::Session.send(:_rate_limited_identity_reset!, cl)
|
|
502
|
+
token = nil
|
|
503
|
+
end
|
|
504
|
+
cl.invalidate_session_identity(token) if token.is_a?(String) && cl.respond_to?(:invalidate_session_identity)
|
|
505
|
+
cl.invalidate_user_identity(owner_id) if owner_id && cl.respond_to?(:invalidate_user_identity)
|
|
506
|
+
rescue StandardError
|
|
507
|
+
# Runs from an `ensure`: never replace the delete's own outcome.
|
|
508
|
+
nil
|
|
509
|
+
end
|
|
207
510
|
end
|
|
208
511
|
end
|
|
@@ -202,6 +202,17 @@ module Parse
|
|
|
202
202
|
class User < Parse::Object
|
|
203
203
|
parse_class Parse::Model::CLASS_USER
|
|
204
204
|
|
|
205
|
+
# ActiveModel naming relative to the `Parse` namespace, so Rails forms,
|
|
206
|
+
# params, routes, and i18n see `Parse::User` as `user`
|
|
207
|
+
# (`params[:user]`, `users_path`) rather than `parse_user`. `name` stays
|
|
208
|
+
# "Parse::User". A subclass outside the `Parse` namespace keeps the
|
|
209
|
+
# default naming.
|
|
210
|
+
# @return [ActiveModel::Name]
|
|
211
|
+
def self.model_name
|
|
212
|
+
return super if name.nil? || !name.start_with?("Parse::")
|
|
213
|
+
@_relative_model_name ||= ActiveModel::Name.new(self, Parse)
|
|
214
|
+
end
|
|
215
|
+
|
|
205
216
|
# When true (default), saving a new {Parse::User} that has a `password`
|
|
206
217
|
# value routes through Parse Server's signup endpoint (`POST /parse/users`)
|
|
207
218
|
# with the `X-Parse-Revocable-Session` header set, so the signup response
|
|
@@ -701,7 +712,7 @@ module Parse
|
|
|
701
712
|
|
|
702
713
|
if response.success?
|
|
703
714
|
result = response.result || {}
|
|
704
|
-
@updated_at = result["updatedAt"] || @updated_at
|
|
715
|
+
@updated_at = _server_date(result["updatedAt"]) || @updated_at
|
|
705
716
|
# Parse Server may rotate the session token on a credential
|
|
706
717
|
# change; apply it narrowly if present without going through the
|
|
707
718
|
# full property writer chain.
|
|
@@ -815,8 +826,8 @@ module Parse
|
|
|
815
826
|
# save-as-signup path had already addressed.
|
|
816
827
|
result = response.result
|
|
817
828
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
818
|
-
@created_at = result["createdAt"] || @created_at
|
|
819
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
829
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
830
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
820
831
|
set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
|
|
821
832
|
# Drop the plaintext password from memory now that the server
|
|
822
833
|
# has it hashed and we no longer need it. Matches the Parse JS
|
|
@@ -908,6 +919,78 @@ module Parse
|
|
|
908
919
|
self.session_token.present?
|
|
909
920
|
end
|
|
910
921
|
|
|
922
|
+
# Replace each non-empty token in `text` with `[FILTERED]`. Used by the
|
|
923
|
+
# `inspect` overrides on {Parse::User} and {Parse::Session} so a session
|
|
924
|
+
# token does not land in logs, exception messages or console history.
|
|
925
|
+
# @!visibility private
|
|
926
|
+
# @param text [String] the inspect output.
|
|
927
|
+
# @param tokens [Array<String, nil>] tokens to redact.
|
|
928
|
+
# @return [String]
|
|
929
|
+
def self.redact_session_token(text, *tokens)
|
|
930
|
+
out = text.to_s
|
|
931
|
+
tokens.each do |t|
|
|
932
|
+
next unless t.is_a?(String) && !t.strip.empty?
|
|
933
|
+
out = out.gsub(t, "[FILTERED]")
|
|
934
|
+
end
|
|
935
|
+
out
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Default inspect output with the session token redacted.
|
|
939
|
+
# @return [String]
|
|
940
|
+
def inspect
|
|
941
|
+
self.class.redact_session_token(super, @session_token, @_session_token)
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# Saves pending changes. When the save changes the password, Parse Server
|
|
945
|
+
# revokes the user's other sessions, so this client's identity plane
|
|
946
|
+
# drops every cached entry for the user to make the revocation visible
|
|
947
|
+
# to mongo-direct reads at once.
|
|
948
|
+
# @!visibility private
|
|
949
|
+
def update!(raw: false, force: false)
|
|
950
|
+
password_changing = changed.include?("password")
|
|
951
|
+
result = super
|
|
952
|
+
ok = raw ? (result.respond_to?(:success?) && result.success?) : result == true
|
|
953
|
+
if ok && password_changing && client.respond_to?(:invalidate_user_identity)
|
|
954
|
+
client.invalidate_user_identity(id)
|
|
955
|
+
end
|
|
956
|
+
result
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Deletes the user. Every cached identity entry for the user is dropped
|
|
960
|
+
# from this client's identity plane, since Parse Server removes the
|
|
961
|
+
# user's sessions with the account. The entries are dropped when the
|
|
962
|
+
# delete returns, successful or not (a failure is usually "object not
|
|
963
|
+
# found": the account is already gone or not visible to the caller, and
|
|
964
|
+
# dropping cached entries is idempotent). A delete that raises leaves
|
|
965
|
+
# them alone.
|
|
966
|
+
# @param session [String] (see Parse::Object#destroy)
|
|
967
|
+
# @return [Boolean] whether the operation was successful.
|
|
968
|
+
def destroy(session: nil)
|
|
969
|
+
user_id = id
|
|
970
|
+
result = super
|
|
971
|
+
begin
|
|
972
|
+
client.invalidate_user_identity(user_id) if user_id.present? && client.respond_to?(:invalidate_user_identity)
|
|
973
|
+
rescue StandardError
|
|
974
|
+
# Never replace the delete's own outcome.
|
|
975
|
+
nil
|
|
976
|
+
end
|
|
977
|
+
result
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# Called by `Array#destroy` for each user in the batch with its
|
|
981
|
+
# response. Drops the user's identity entries, as {#destroy} does, when
|
|
982
|
+
# the delete succeeded or the row is already gone; a denied delete
|
|
983
|
+
# leaves them alone.
|
|
984
|
+
# @!visibility private
|
|
985
|
+
def _after_batch_destroy(response = nil)
|
|
986
|
+
applied = response.nil? ||
|
|
987
|
+
(response.respond_to?(:success?) && response.success?) ||
|
|
988
|
+
(response.respond_to?(:object_not_found?) && response.object_not_found?)
|
|
989
|
+
return unless applied
|
|
990
|
+
client.invalidate_user_identity(id) if id.present? && client.respond_to?(:invalidate_user_identity)
|
|
991
|
+
end
|
|
992
|
+
private :_after_batch_destroy
|
|
993
|
+
|
|
911
994
|
# Invalid the current session token for this logged in user.
|
|
912
995
|
# @return [Boolean] True/false if successful
|
|
913
996
|
def logout
|
|
@@ -1016,7 +1099,10 @@ module Parse
|
|
|
1016
1099
|
# the request.
|
|
1017
1100
|
#
|
|
1018
1101
|
# @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
|
|
1019
|
-
# @option opts [Boolean] :
|
|
1102
|
+
# @option opts [Boolean] :use_master_key send the master key with the
|
|
1103
|
+
# signup. Off by default: a master-key signup returns no session token
|
|
1104
|
+
# and bypasses the `_User` create CLP. Pass `true` only for admin
|
|
1105
|
+
# provisioning that needs that bypass.
|
|
1020
1106
|
# @raise [ArgumentError] If `body` contains `authData`/`auth_data`/`objectId` — use {.autologin_service} for federated flows.
|
|
1021
1107
|
# @raise [Parse::Error::UsernameMissingError] If username is missing.
|
|
1022
1108
|
# @raise [Parse::Error::PasswordMissingError] If password is missing.
|
|
@@ -1145,6 +1231,8 @@ module Parse
|
|
|
1145
1231
|
# @param username [String] the user's username
|
|
1146
1232
|
# @param password [String] the user's password
|
|
1147
1233
|
# @return [User] a logged in user for the provided username. Returns nil otherwise.
|
|
1234
|
+
# @raise [Parse::MFA::RequiredError] when the account has MFA enabled; use
|
|
1235
|
+
# {.login_with_mfa} with the user's code.
|
|
1148
1236
|
# @see .login!
|
|
1149
1237
|
def self.login(username, password)
|
|
1150
1238
|
response = client.login(username.to_s, password.to_s)
|
|
@@ -1350,6 +1438,13 @@ module Parse
|
|
|
1350
1438
|
end
|
|
1351
1439
|
n
|
|
1352
1440
|
end
|
|
1441
|
+
# Every session but (optionally) the current one is gone server-side.
|
|
1442
|
+
# Drop the user's identity entries on this client and on the default
|
|
1443
|
+
# client (the logout above went through `Parse.client`). A kept
|
|
1444
|
+
# current token simply re-resolves on its next use.
|
|
1445
|
+
[client, Parse.client].uniq.each do |cl|
|
|
1446
|
+
cl.invalidate_user_identity(id) if cl.respond_to?(:invalidate_user_identity)
|
|
1447
|
+
end
|
|
1353
1448
|
@session_token = nil unless keep_current
|
|
1354
1449
|
@session = nil unless keep_current
|
|
1355
1450
|
count
|
|
@@ -1648,14 +1743,15 @@ module Parse
|
|
|
1648
1743
|
# Cloud Code `beforeSave(_User)` see `request.user = caller`,
|
|
1649
1744
|
# which an integrator can mistake for "the new user". The signup
|
|
1650
1745
|
# endpoint authenticates by the signup itself, not by a prior
|
|
1651
|
-
# session
|
|
1652
|
-
#
|
|
1746
|
+
# session, so pass `nil` explicitly. The master key is not sent
|
|
1747
|
+
# either (see Parse::API::Users#create_user): Parse Server mints no
|
|
1748
|
+
# session token for a master-key signup.
|
|
1653
1749
|
res = client.create_user(body, session_token: nil)
|
|
1654
1750
|
unless res.error?
|
|
1655
1751
|
result = res.result
|
|
1656
1752
|
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
1657
|
-
@created_at = result["createdAt"] || @created_at
|
|
1658
|
-
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
1753
|
+
@created_at = _server_date(result["createdAt"]) || @created_at
|
|
1754
|
+
@updated_at = _server_date(result["updatedAt"] || result["createdAt"]) || @updated_at
|
|
1659
1755
|
# Plaintext password is no longer needed locally; the server
|
|
1660
1756
|
# has it hashed. Direct ivar assignment avoids re-dirtying the
|
|
1661
1757
|
# field.
|