parse-stack-next 5.3.0 → 5.4.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/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
|
@@ -155,7 +155,7 @@ module Parse
|
|
|
155
155
|
},
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
response = client.update_user(id, { authData: auth_data_payload },
|
|
158
|
+
response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
|
|
159
159
|
|
|
160
160
|
if response.error?
|
|
161
161
|
if response.result.to_s.include?("Invalid MFA")
|
|
@@ -208,7 +208,7 @@ module Parse
|
|
|
208
208
|
},
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
response = client.update_user(id, { authData: auth_data_payload },
|
|
211
|
+
response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
|
|
212
212
|
|
|
213
213
|
if response.error?
|
|
214
214
|
raise Parse::Client::ResponseError, response
|
|
@@ -245,7 +245,7 @@ module Parse
|
|
|
245
245
|
},
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
response = client.update_user(id, { authData: auth_data_payload },
|
|
248
|
+
response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
|
|
249
249
|
|
|
250
250
|
if response.error?
|
|
251
251
|
if response.result.to_s.include?("Invalid MFA token")
|
|
@@ -276,27 +276,60 @@ module Parse
|
|
|
276
276
|
raise MFA::NotEnabledError, "MFA is not enabled for this user" unless mfa_enabled?
|
|
277
277
|
raise ArgumentError, "Current token is required" if current_token.blank?
|
|
278
278
|
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
279
|
+
# Parse Server's TOTP adapter exposes no first-class "disable via authData
|
|
280
|
+
# update" path — its validateUpdate always re-runs setup, so a partial
|
|
281
|
+
# mfa payload is rejected outright. Disabling is therefore a two-step:
|
|
282
|
+
#
|
|
283
|
+
# 1. Prove possession of the current code by submitting it as
|
|
284
|
+
# `{ mfa: { old: <token> } }`. In the *update* context (unlike a
|
|
285
|
+
# fresh login) the adapter validates that code against the stored
|
|
286
|
+
# secret. A WRONG code fails at validateLogin ("Invalid MFA token");
|
|
287
|
+
# a CORRECT code passes validateLogin and is then blocked by the
|
|
288
|
+
# re-setup requirement ("Invalid MFA data") — which is precisely the
|
|
289
|
+
# signal that the code was accepted. (This re-entry of the current
|
|
290
|
+
# code is the deliberate confirmation gate for turning MFA off.)
|
|
291
|
+
# 2. Disable MFA by unlinking the provider with `{ mfa: nil }`.
|
|
292
|
+
#
|
|
293
|
+
# This keeps self-disable gated on a valid current code even though the
|
|
294
|
+
# server offers no dedicated TOTP self-disable endpoint.
|
|
295
|
+
verify = client.update_user(id, { authData: { mfa: { old: current_token } } },
|
|
296
|
+
session_token: session_token)
|
|
297
|
+
# Classify the two-step response POSITIVELY instead of treating
|
|
298
|
+
# "anything that isn't success-or-one-magic-string" as a bad
|
|
299
|
+
# token. The current code is ACCEPTED iff the server either
|
|
300
|
+
# succeeds or rejects only the follow-on re-setup ("Invalid MFA
|
|
301
|
+
# data") — that block fires AFTER validateLogin has already
|
|
302
|
+
# accepted the code. A WRONG code fails earlier at validateLogin
|
|
303
|
+
# ("Invalid MFA token"). Any OTHER error (transport, session, 5xx)
|
|
304
|
+
# is a real fault surfaced as-is, not mislabeled a verification
|
|
305
|
+
# failure.
|
|
306
|
+
err = verify.error.to_s
|
|
307
|
+
code_rejected = err.match?(/Invalid MFA token/i)
|
|
308
|
+
code_accepted = verify.success? || err.match?(/Invalid MFA data/i)
|
|
309
|
+
if code_rejected
|
|
310
|
+
raise MFA::VerificationError, "Invalid MFA token"
|
|
311
|
+
elsif !code_accepted
|
|
312
|
+
raise Parse::Client::ResponseError, verify
|
|
295
313
|
end
|
|
296
314
|
|
|
297
|
-
|
|
298
|
-
|
|
315
|
+
response = client.update_user(id, { authData: { mfa: nil } }, session_token: session_token)
|
|
316
|
+
raise Parse::Client::ResponseError, response if response.error?
|
|
317
|
+
|
|
318
|
+
# CONFIRM the disable took effect from the SERVER's own view — a
|
|
319
|
+
# positive post-condition rather than trusting the unlink response
|
|
320
|
+
# alone. We must read the server directly here, NOT lean on the
|
|
321
|
+
# in-memory #mfa_enabled? projection: Parse Server omits +authData+
|
|
322
|
+
# entirely for a user with no providers, so once MFA is unlinked an
|
|
323
|
+
# ordinary fetch carries no +authData+ key at all and therefore can
|
|
324
|
+
# never clear the +{ mfa: { status: "enabled" } }+ value pinned at
|
|
325
|
+
# enrollment. An enabled account's own (session-token) read returns
|
|
326
|
+
# +authData.mfa+; a disabled one omits it — so an absent/mfa-less
|
|
327
|
+
# authData on this trusted self-read is the authoritative signal.
|
|
328
|
+
if mfa_enabled_on_server?
|
|
329
|
+
raise MFA::VerificationError, "MFA disable did not take effect (still enabled after unlink)"
|
|
330
|
+
end
|
|
299
331
|
|
|
332
|
+
clear_local_mfa_projection!
|
|
300
333
|
true
|
|
301
334
|
end
|
|
302
335
|
|
|
@@ -315,20 +348,26 @@ module Parse
|
|
|
315
348
|
#
|
|
316
349
|
# @param authorized_by [Parse::User, Parse::Pointer] the operator
|
|
317
350
|
# performing the override. Required.
|
|
318
|
-
# @param admin_role [Parse::Role, String, nil]
|
|
319
|
-
#
|
|
351
|
+
# @param admin_role [Parse::Role, String, nil] role (or role name)
|
|
352
|
+
# that +authorized_by+ must belong to. Library-enforced. Either
|
|
353
|
+
# this or +allow_unverified: true+ is REQUIRED (fail-closed).
|
|
354
|
+
# @param allow_unverified [Boolean] explicitly accept caller-side
|
|
355
|
+
# authorization without a library role check. Defaults to +false+;
|
|
356
|
+
# must be set deliberately to bypass MFA without an +admin_role+.
|
|
320
357
|
# @return [Boolean] True if disabled successfully.
|
|
321
358
|
# @raise [ArgumentError] when +authorized_by:+ is missing or not a User.
|
|
322
|
-
# @raise [Parse::MFA::ForbiddenError] when +admin_role+
|
|
359
|
+
# @raise [Parse::MFA::ForbiddenError] when neither +admin_role+ nor
|
|
360
|
+
# +allow_unverified:+ is supplied, or when +admin_role+ is supplied
|
|
323
361
|
# and the operator is not a member.
|
|
324
362
|
#
|
|
325
|
-
# @example
|
|
326
|
-
# user.disable_mfa_master_key!(authorized_by: current_admin)
|
|
327
|
-
#
|
|
328
|
-
# @example Library-enforced role check
|
|
363
|
+
# @example Library-enforced role check (preferred)
|
|
329
364
|
# user.disable_mfa_master_key!(authorized_by: current_admin,
|
|
330
365
|
# admin_role: "Admin")
|
|
331
|
-
|
|
366
|
+
#
|
|
367
|
+
# @example Caller-verified authorization (explicit opt-out)
|
|
368
|
+
# user.disable_mfa_master_key!(authorized_by: current_admin,
|
|
369
|
+
# allow_unverified: true)
|
|
370
|
+
def disable_mfa_master_key!(authorized_by:, admin_role: nil, allow_unverified: false)
|
|
332
371
|
operator = authorized_by
|
|
333
372
|
unless operator.is_a?(Parse::User) ||
|
|
334
373
|
(operator.is_a?(Parse::Pointer) && operator.parse_class == Parse::User.parse_class)
|
|
@@ -340,6 +379,18 @@ module Parse
|
|
|
340
379
|
raise ArgumentError, "authorized_by: User must be persisted (have an objectId)"
|
|
341
380
|
end
|
|
342
381
|
|
|
382
|
+
# FAIL CLOSED: this method bypasses MFA verification entirely via
|
|
383
|
+
# the master key, so it refuses to run without SOME authorization
|
|
384
|
+
# signal. Either supply an `admin_role:` for the library to verify,
|
|
385
|
+
# or pass `allow_unverified: true` to deliberately assert that the
|
|
386
|
+
# caller has already authorized the operator out-of-band.
|
|
387
|
+
if admin_role.nil? && !allow_unverified
|
|
388
|
+
raise MFA::ForbiddenError,
|
|
389
|
+
"disable_mfa_master_key! refuses to bypass MFA without an authorization " \
|
|
390
|
+
"check: pass admin_role: to enforce role membership, or " \
|
|
391
|
+
"allow_unverified: true to explicitly accept caller-side authorization."
|
|
392
|
+
end
|
|
393
|
+
|
|
343
394
|
if admin_role
|
|
344
395
|
role = admin_role.is_a?(Parse::Role) ? admin_role : Parse::Role.find_by_name(admin_role.to_s)
|
|
345
396
|
if role.nil?
|
|
@@ -357,14 +408,19 @@ module Parse
|
|
|
357
408
|
end
|
|
358
409
|
|
|
359
410
|
auth_data_payload = { mfa: nil }
|
|
360
|
-
response = client.update_user(id, { authData: auth_data_payload },
|
|
411
|
+
response = client.update_user(id, { authData: auth_data_payload }, use_master_key: true)
|
|
361
412
|
|
|
362
413
|
if response.error?
|
|
363
414
|
raise Parse::Client::ResponseError, response
|
|
364
415
|
end
|
|
365
416
|
|
|
366
|
-
# Refresh auth_data
|
|
417
|
+
# Refresh auth_data, then drop the in-memory MFA projection. As in
|
|
418
|
+
# #disable_mfa!, a disabled user's read omits +authData+, so the
|
|
419
|
+
# +{ mfa: { status: "enabled" } }+ value pinned at enrollment won't
|
|
420
|
+
# self-clear on fetch — clear it explicitly so #mfa_enabled? reports
|
|
421
|
+
# the truth after a master-key disable.
|
|
367
422
|
fetch
|
|
423
|
+
clear_local_mfa_projection!
|
|
368
424
|
|
|
369
425
|
true
|
|
370
426
|
end
|
|
@@ -435,6 +491,42 @@ module Parse
|
|
|
435
491
|
account_name = email.presence || username.presence || id
|
|
436
492
|
MFA.qr_code(secret, account_name, issuer: issuer, format: format)
|
|
437
493
|
end
|
|
494
|
+
|
|
495
|
+
private
|
|
496
|
+
|
|
497
|
+
# @!visibility private
|
|
498
|
+
# Authoritative server-side MFA check via a trusted self-read.
|
|
499
|
+
# Reads +authData.mfa+ straight from a fresh session-token fetch
|
|
500
|
+
# rather than the (possibly stale) in-memory projection. An enabled
|
|
501
|
+
# account returns +authData.mfa+ with a +status+/+secret+; a disabled
|
|
502
|
+
# one omits +authData+ — so absence (or an mfa-less authData) means
|
|
503
|
+
# disabled.
|
|
504
|
+
# @return [Boolean]
|
|
505
|
+
def mfa_enabled_on_server?
|
|
506
|
+
result = client.fetch_object(self.class.parse_class, id,
|
|
507
|
+
session_token: session_token).result
|
|
508
|
+
mfa = result.is_a?(Hash) ? result["authData"] : nil
|
|
509
|
+
mfa = mfa["mfa"] if mfa.is_a?(Hash)
|
|
510
|
+
mfa.is_a?(Hash) && (mfa["status"] == "enabled" || mfa["secret"].present?)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# @!visibility private
|
|
514
|
+
# Drop the in-memory MFA projection after a disable. A disabled user's
|
|
515
|
+
# server read omits +authData+ entirely, so an ordinary fetch can
|
|
516
|
+
# never clear the +{ mfa: { status: "enabled" } }+ value pinned at
|
|
517
|
+
# enrollment; do it explicitly here. Only the +mfa+ subkey is removed
|
|
518
|
+
# (any anonymous/OAuth authData is preserved), and the assignment runs
|
|
519
|
+
# through the non-dirtying hydration path inside a +with_authdata_trust+
|
|
520
|
+
# scope so it is neither stripped nor marked dirty — a later #save will
|
|
521
|
+
# not resend +authData+.
|
|
522
|
+
def clear_local_mfa_projection!
|
|
523
|
+
cleared = auth_data.is_a?(Hash) ? auth_data.dup : {}
|
|
524
|
+
cleared.delete("mfa")
|
|
525
|
+
cleared.delete(:mfa)
|
|
526
|
+
self.class.with_authdata_trust do
|
|
527
|
+
apply_attributes!({ "authData" => cleared }, dirty_track: false)
|
|
528
|
+
end
|
|
529
|
+
end
|
|
438
530
|
end
|
|
439
531
|
|
|
440
532
|
# Not enabled error
|