parse-stack-next 5.5.3 → 5.5.5
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 +13 -4
- data/README.md +26 -13
- data/bin/parse-console +9 -1
- data/docs/TEST_SERVER.md +115 -238
- data/docs/mcp_guide.md +1 -1
- data/docs/mongodb_index_optimization_guide.md +3 -2
- data/docs/usage_guide.md +1 -1
- data/docs/yard-template/default/fulldoc/html/css/common.css +52 -9
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +40 -13
- data/lib/parse/agent/constraint_translator.rb +18 -18
- data/lib/parse/agent/errors.rb +29 -7
- data/lib/parse/agent/metadata_dsl.rb +6 -6
- data/lib/parse/agent/tools.rb +250 -59
- data/lib/parse/agent.rb +42 -30
- data/lib/parse/api/aggregate.rb +3 -3
- data/lib/parse/api/cloud_functions.rb +19 -10
- data/lib/parse/api/objects.rb +8 -8
- data/lib/parse/api/users.rb +9 -9
- data/lib/parse/atlas_search/session.rb +34 -34
- data/lib/parse/atlas_search.rb +243 -110
- data/lib/parse/client/body_builder.rb +10 -10
- data/lib/parse/client/logging.rb +5 -2
- data/lib/parse/client/profiling.rb +5 -2
- data/lib/parse/client/protocol.rb +1 -1
- data/lib/parse/client/url_redaction.rb +94 -0
- data/lib/parse/client.rb +43 -28
- data/lib/parse/embeddings/image_fetch.rb +6 -1
- data/lib/parse/embeddings/voyage.rb +16 -17
- data/lib/parse/live_query/client.rb +7 -7
- data/lib/parse/live_query/subscription.rb +1 -1
- data/lib/parse/lock.rb +1 -1
- data/lib/parse/lock_backend.rb +118 -2
- data/lib/parse/model/acl.rb +24 -24
- data/lib/parse/model/classes/job_schedule.rb +8 -8
- data/lib/parse/model/classes/job_status.rb +9 -9
- data/lib/parse/model/classes/role.rb +49 -49
- data/lib/parse/model/classes/session.rb +2 -2
- data/lib/parse/model/classes/user.rb +66 -66
- data/lib/parse/model/core/builder.rb +7 -7
- data/lib/parse/model/core/create_lock.rb +1 -1
- data/lib/parse/model/core/properties.rb +4 -4
- data/lib/parse/model/file.rb +57 -16
- data/lib/parse/model/model.rb +19 -19
- data/lib/parse/model/object.rb +38 -38
- data/lib/parse/model/pointer.rb +4 -4
- data/lib/parse/model/push.rb +5 -5
- data/lib/parse/mongodb.rb +84 -26
- data/lib/parse/pipeline_security.rb +2 -2
- data/lib/parse/query/constraints.rb +38 -38
- data/lib/parse/query.rb +151 -75
- data/lib/parse/retrieval/reranker/cohere.rb +30 -0
- data/lib/parse/schema.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -10
- data/lib/parse/two_factor_auth/user_extension.rb +25 -25
- data/lib/parse/webhooks/payload.rb +35 -35
- data/lib/parse/webhooks/registration.rb +2 -2
- data/lib/parse/webhooks/replay_protection.rb +16 -16
- data/lib/parse/webhooks.rb +11 -11
- data/parse-stack-next.gemspec +19 -1
- metadata +2 -38
- data/.bundle/config +0 -5
- data/.env.sample +0 -138
- data/.env.test +0 -10
- data/.github/ISSUE_TEMPLATE/bug_report.yml +0 -105
- data/.github/ISSUE_TEMPLATE/feature_request.yml +0 -67
- data/.github/dependabot.yml +0 -13
- data/.github/workflows/codeql.yml +0 -44
- data/.github/workflows/docs.yml +0 -39
- data/.github/workflows/release.yml +0 -43
- data/.github/workflows/ruby.yml +0 -38
- data/.gitignore +0 -56
- data/.ruby-version +0 -1
- data/.solargraph.yml +0 -22
- data/.vscode/settings.json +0 -3
- data/.yardopts +0 -19
- data/Gemfile +0 -43
- data/Gemfile.lock +0 -198
- data/Makefile +0 -63
- data/Rakefile +0 -825
- data/config/parse-config.json +0 -12
- data/scripts/debug-ips.js +0 -35
- data/scripts/docker/Dockerfile.parse +0 -17
- data/scripts/docker/atlas-init.js +0 -284
- data/scripts/docker/docker-compose.atlas.yml +0 -80
- data/scripts/docker/docker-compose.test.yml +0 -159
- data/scripts/docker/docker-compose.verifyemail.yml +0 -4
- data/scripts/docker/mongo-init.js +0 -21
- data/scripts/docker/preflight.sh +0 -76
- data/scripts/eval_mcp_with_lm_studio.rb +0 -274
- data/scripts/start-parse.sh +0 -154
- data/scripts/start_mcp_server.rb +0 -78
- data/scripts/test_server_connection.rb +0 -82
- data/scripts/vector_prototype/create_vector_index.js +0 -105
- data/scripts/vector_prototype/fetch_embeddings.py +0 -241
- data/scripts/vector_prototype/fixture_manifest.json +0 -9
- data/scripts/vector_prototype/query_prototype.rb +0 -84
- data/scripts/vector_prototype/run.sh +0 -34
|
@@ -31,13 +31,13 @@ module Parse
|
|
|
31
31
|
|
|
32
32
|
# Error code 205 (Parse::Response::ERROR_EMAIL_NOT_FOUND) raised by
|
|
33
33
|
# {Parse::User.login!} and {Parse::User#verify_password} when Parse Server
|
|
34
|
-
# returns code 205 because
|
|
34
|
+
# returns code 205 because `preventLoginWithUnverifiedEmail` is enabled and
|
|
35
35
|
# the account's email address has not been verified.
|
|
36
36
|
#
|
|
37
37
|
# It is a SUBCLASS of {AuthenticationError} on purpose: before this typed
|
|
38
38
|
# error existed, the unverified-email rejection raised a plain
|
|
39
|
-
#
|
|
40
|
-
# in
|
|
39
|
+
# `AuthenticationError`, so existing callers wrapping {Parse::User.login!}
|
|
40
|
+
# in `rescue AuthenticationError` must keep catching it (subclassing keeps
|
|
41
41
|
# that contract — making it a sibling would be a silent breaking change).
|
|
42
42
|
# Callers who want to special-case the unverified-email path just rescue
|
|
43
43
|
# this narrower subclass FIRST.
|
|
@@ -62,12 +62,12 @@ module Parse
|
|
|
62
62
|
# <tt>rescue Parse::Error::AuthenticationError</tt> handler covers both
|
|
63
63
|
# wrong-credential failures and lockout situations. Callers that need to
|
|
64
64
|
# distinguish the lockout case just rescue this narrower subclass first.
|
|
65
|
-
# Because the previous implementation raised a plain
|
|
66
|
-
# is no prior
|
|
65
|
+
# Because the previous implementation raised a plain `RuntimeError`, there
|
|
66
|
+
# is no prior `AuthenticationError` rescue contract to preserve — this is
|
|
67
67
|
# a new typed entry in the login-failure taxonomy.
|
|
68
68
|
#
|
|
69
|
-
# Note that
|
|
70
|
-
#
|
|
69
|
+
# Note that `Parse::Error < StandardError`, so a bare `rescue` or
|
|
70
|
+
# `rescue StandardError` still catches this error.
|
|
71
71
|
#
|
|
72
72
|
# @example
|
|
73
73
|
# begin
|
|
@@ -266,7 +266,7 @@ module Parse
|
|
|
266
266
|
# when the user follows the verification link delivered by the email
|
|
267
267
|
# adapter, and applied to the in-memory object by {#signup!} / signup-on-save
|
|
268
268
|
# when the server includes it in the signup response (see
|
|
269
|
-
#
|
|
269
|
+
# `SIGNUP_RESPONSE_APPLY_KEYS`).
|
|
270
270
|
# @return [Boolean]
|
|
271
271
|
property :email_verified, :boolean
|
|
272
272
|
|
|
@@ -322,16 +322,16 @@ module Parse
|
|
|
322
322
|
# Thread-local key used by {.with_authdata_trust} to mark the
|
|
323
323
|
# current hydration as a legitimate self-fetch (login/signup/MFA/
|
|
324
324
|
# `/users/me`). Outside that scope, {#apply_attributes!} strips
|
|
325
|
-
#
|
|
325
|
+
# `authData` from incoming server JSON so a `_User` query/find that
|
|
326
326
|
# crosses ACL boundaries cannot leak another user's federated-identity
|
|
327
327
|
# tokens into the in-memory object.
|
|
328
328
|
AUTHDATA_TRUST_KEY = :__parse_stack_user_authdata_trusted
|
|
329
329
|
|
|
330
330
|
class << self
|
|
331
331
|
# @!visibility private
|
|
332
|
-
# Run
|
|
333
|
-
# call is permitted to hydrate
|
|
334
|
-
#
|
|
332
|
+
# Run `block` in a scope where the next {Parse::User#apply_attributes!}
|
|
333
|
+
# call is permitted to hydrate `authData` from the response. Used by
|
|
334
|
+
# `login`/`login!`/`session!`/`create`/`link_auth_data!`/MFA paths
|
|
335
335
|
# where the row being hydrated is provably the authenticating user.
|
|
336
336
|
def with_authdata_trust
|
|
337
337
|
prior = Thread.current[AUTHDATA_TRUST_KEY]
|
|
@@ -538,20 +538,20 @@ module Parse
|
|
|
538
538
|
end
|
|
539
539
|
|
|
540
540
|
# @!visibility private
|
|
541
|
-
# Defense-in-depth strip of
|
|
542
|
-
# User. Parse Server returns
|
|
543
|
-
# caller with ACL read on the row, and the default
|
|
541
|
+
# Defense-in-depth strip of `authData` on the way into the in-memory
|
|
542
|
+
# User. Parse Server returns `authData` on `GET /users/:id` to any
|
|
543
|
+
# caller with ACL read on the row, and the default `_User` ACL is
|
|
544
544
|
# permissive in many deployments — without this filter, fetching a
|
|
545
|
-
# different user (or iterating a
|
|
546
|
-
# would expose their OAuth
|
|
545
|
+
# different user (or iterating a `Parse::Query.new(User)` result set)
|
|
546
|
+
# would expose their OAuth `access_token` / `id_token` to anyone who
|
|
547
547
|
# JSON-renders the result (Rails views, agent tool output, logging).
|
|
548
548
|
#
|
|
549
549
|
# The strip runs unconditionally unless the caller is inside a
|
|
550
550
|
# {.with_authdata_trust} scope, which is set by the self-fetch
|
|
551
551
|
# paths in this file (login/login!/session!/create/link_auth_data!/
|
|
552
552
|
# unlink_auth_data!) and by the MFA login extension. Trusted callers
|
|
553
|
-
# pass through to
|
|
554
|
-
# mass-assignment filter that runs inside
|
|
553
|
+
# pass through to `super` with the hash untouched. The PROTECTED
|
|
554
|
+
# mass-assignment filter that runs inside `super` is unaffected.
|
|
555
555
|
def apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil)
|
|
556
556
|
if hash.is_a?(Hash) && !self.class.authdata_trusted?
|
|
557
557
|
if hash.key?(:authData) || hash.key?("authData") ||
|
|
@@ -576,12 +576,12 @@ module Parse
|
|
|
576
576
|
end
|
|
577
577
|
|
|
578
578
|
# @!visibility private
|
|
579
|
-
# Reduce a server-returned
|
|
580
|
-
# Parse Server returns
|
|
579
|
+
# Reduce a server-returned `authData` hash to a leak-safe MFA status.
|
|
580
|
+
# Parse Server returns `authData.mfa` as +{ "secret" => ..., "recovery" =>
|
|
581
581
|
# [...] }+ (the raw TOTP secret and one-time recovery codes) even on a
|
|
582
582
|
# user's own session-token read, so the value itself must never be retained.
|
|
583
|
-
# This keeps only
|
|
584
|
-
# configured, and returns
|
|
583
|
+
# This keeps only `{ "mfa" => { "status" => "enabled" } }` when MFA is
|
|
584
|
+
# configured, and returns `nil` otherwise (preserving the prior
|
|
585
585
|
# strip-to-nil behavior for OAuth-only / non-MFA authData).
|
|
586
586
|
# @return [Hash, nil]
|
|
587
587
|
def sanitized_mfa_authdata(raw)
|
|
@@ -607,7 +607,7 @@ module Parse
|
|
|
607
607
|
end
|
|
608
608
|
|
|
609
609
|
# @return [Boolean] true if this user is anonymous (i.e. created
|
|
610
|
-
# via the
|
|
610
|
+
# via the `authData.anonymous` provider rather than via signup
|
|
611
611
|
# with a username/password or a real OAuth provider).
|
|
612
612
|
def anonymous?
|
|
613
613
|
!anonymous_id.nil?
|
|
@@ -640,15 +640,15 @@ module Parse
|
|
|
640
640
|
self.class.with_authdata_trust { apply_attributes!(response.result) }
|
|
641
641
|
end
|
|
642
642
|
|
|
643
|
-
# Upgrade an anonymous user (one created via the
|
|
643
|
+
# Upgrade an anonymous user (one created via the `authData.anonymous`
|
|
644
644
|
# provider) into a full username/password account. This is the
|
|
645
645
|
# SDK-side counterpart of the Parse JS SDK's
|
|
646
|
-
#
|
|
647
|
-
#
|
|
648
|
-
#
|
|
646
|
+
# `_linkWith('username', ...)` flow — it sends a single
|
|
647
|
+
# `PUT /users/:id` with the new credentials and an explicit
|
|
648
|
+
# `authData: { anonymous: nil }` unlink in the same body, then
|
|
649
649
|
# narrowly applies the server's response to the in-memory user.
|
|
650
650
|
#
|
|
651
|
-
# The
|
|
651
|
+
# The `authData.anonymous` unlink is essential: leaving the anonymous
|
|
652
652
|
# provider attached after assigning a username would let anyone else
|
|
653
653
|
# who somehow learned the (random) anonymous id silently log in as
|
|
654
654
|
# the freshly-named account, a documented Parse foot-gun.
|
|
@@ -658,9 +658,9 @@ module Parse
|
|
|
658
658
|
# @param email [String, nil] optional email address. Must be unique
|
|
659
659
|
# if provided.
|
|
660
660
|
# @raise [Parse::Error::AuthenticationError] when this instance has
|
|
661
|
-
# no attached
|
|
662
|
-
# @raise [Parse::Error::UsernameMissingError] when
|
|
663
|
-
# @raise [Parse::Error::PasswordMissingError] when
|
|
661
|
+
# no attached `@session_token`, no objectId, or is not anonymous.
|
|
662
|
+
# @raise [Parse::Error::UsernameMissingError] when `username` is blank.
|
|
663
|
+
# @raise [Parse::Error::PasswordMissingError] when `password` is blank.
|
|
664
664
|
# @raise [Parse::Error::UsernameTakenError] when Parse Server reports
|
|
665
665
|
# the username already exists.
|
|
666
666
|
# @raise [Parse::Error::EmailTakenError] when Parse Server reports
|
|
@@ -929,9 +929,9 @@ module Parse
|
|
|
929
929
|
def session
|
|
930
930
|
if @session.blank? && @session_token.present?
|
|
931
931
|
response = client.fetch_session(@session_token)
|
|
932
|
-
# Trusted hydration:
|
|
933
|
-
# _Session row, which legitimately includes
|
|
934
|
-
#
|
|
932
|
+
# Trusted hydration: `response.result` is the server-side
|
|
933
|
+
# _Session row, which legitimately includes `sessionToken`,
|
|
934
|
+
# `createdAt`, `updatedAt`, and other protected keys. Route
|
|
935
935
|
# through {Parse::Object.build} which handles the trusted-init
|
|
936
936
|
# signalling.
|
|
937
937
|
@session ||= Parse::Object.build(response.result, Parse::Model::CLASS_SESSION)
|
|
@@ -940,9 +940,9 @@ module Parse
|
|
|
940
940
|
end
|
|
941
941
|
|
|
942
942
|
# A non-master {Parse::Client} bound to this user's session token, for
|
|
943
|
-
# acting on the server *as this user* with full ACL / CLP /
|
|
943
|
+
# acting on the server *as this user* with full ACL / CLP / `protectedFields`
|
|
944
944
|
# enforcement and no master-key fallback. It mirrors the connection settings
|
|
945
|
-
# of
|
|
945
|
+
# of `base` (the configured client by default) but carries no master key and
|
|
946
946
|
# binds {#session_token}, so even raw REST calls through it are authorized as
|
|
947
947
|
# the user with no per-call ceremony. The web-counterpart of
|
|
948
948
|
# {Parse::Webhooks::Payload#user_client}; the typical client-side entry point
|
|
@@ -952,7 +952,7 @@ module Parse
|
|
|
952
952
|
# Parse::Query.new("Post", client: client).results # scoped to the user
|
|
953
953
|
#
|
|
954
954
|
# @param base [Parse::Client] the client whose connection settings to mirror.
|
|
955
|
-
# @return [Parse::Client, nil]
|
|
955
|
+
# @return [Parse::Client, nil] `nil` when the user has no session token
|
|
956
956
|
# (e.g. fetched/saved under the master key rather than logged in).
|
|
957
957
|
def session_client(base = self.client)
|
|
958
958
|
return nil if @session_token.nil? || @session_token.to_s.strip.empty?
|
|
@@ -960,23 +960,23 @@ module Parse
|
|
|
960
960
|
end
|
|
961
961
|
|
|
962
962
|
# @!visibility private
|
|
963
|
-
# Keys that must never flow through
|
|
964
|
-
# mass-assigned hash.
|
|
963
|
+
# Keys that must never flow through `Parse::User.create` from a
|
|
964
|
+
# mass-assigned hash. `authData` on the user-signup endpoint causes
|
|
965
965
|
# Parse Server to silently log into the existing account that matches
|
|
966
966
|
# that auth_data and return ITS sessionToken — full account takeover
|
|
967
967
|
# if the caller blindly forwards client-supplied parameters.
|
|
968
|
-
#
|
|
968
|
+
# `objectId` allows the caller to pick the user's identifier on
|
|
969
969
|
# creation, sometimes targetable depending on Parse Server config.
|
|
970
970
|
UNSAFE_CREATE_KEYS = %i[authData auth_data objectId id].freeze
|
|
971
971
|
|
|
972
972
|
# @!visibility private
|
|
973
973
|
# Fields that are server-controlled and must be stripped from any body
|
|
974
|
-
# that the SDK sends to the signup endpoint or
|
|
974
|
+
# that the SDK sends to the signup endpoint or `Parse::User.create`,
|
|
975
975
|
# regardless of who supplied them. Unlike {UNSAFE_CREATE_KEYS}, passing
|
|
976
976
|
# one of these is not refused (no exception is raised); the field is
|
|
977
977
|
# silently dropped before wire transit.
|
|
978
978
|
#
|
|
979
|
-
#
|
|
979
|
+
# `emailVerified` is the canonical case: Parse Server's default `_User`
|
|
980
980
|
# CLP restricts writes to the master key, so a caller-supplied value
|
|
981
981
|
# would normally be rejected anyway — but the SDK strips it as
|
|
982
982
|
# defense-in-depth so signup with mass-assigned attributes cannot
|
|
@@ -1008,7 +1008,7 @@ module Parse
|
|
|
1008
1008
|
|
|
1009
1009
|
# Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.
|
|
1010
1010
|
#
|
|
1011
|
-
# Mass-assignment of
|
|
1011
|
+
# Mass-assignment of `authData`/`auth_data`/`objectId` is refused. If you
|
|
1012
1012
|
# intend to create-or-login a user via federated identity, use
|
|
1013
1013
|
# {.autologin_service} or {.link_or_create_with_auth_data}. Passing
|
|
1014
1014
|
# those keys directly bypasses the SDK's federated-identity wrapper
|
|
@@ -1017,7 +1017,7 @@ module Parse
|
|
|
1017
1017
|
#
|
|
1018
1018
|
# @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
|
|
1019
1019
|
# @option opts [Boolean] :master_key Whether the master key should be used for this request.
|
|
1020
|
-
# @raise [ArgumentError] If
|
|
1020
|
+
# @raise [ArgumentError] If `body` contains `authData`/`auth_data`/`objectId` — use {.autologin_service} for federated flows.
|
|
1021
1021
|
# @raise [Parse::Error::UsernameMissingError] If username is missing.
|
|
1022
1022
|
# @raise [Parse::Error::PasswordMissingError] If password is missing.
|
|
1023
1023
|
# @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
|
|
@@ -1057,11 +1057,11 @@ module Parse
|
|
|
1057
1057
|
end
|
|
1058
1058
|
|
|
1059
1059
|
# @!visibility private
|
|
1060
|
-
# Silently strips {SERVER_CONTROLLED_KEYS} from
|
|
1060
|
+
# Silently strips {SERVER_CONTROLLED_KEYS} from `body` in place. Used
|
|
1061
1061
|
# by {.create}, {#signup!}, and {#signup_create} as defense-in-depth so
|
|
1062
1062
|
# caller-supplied values for fields that Parse Server is meant to
|
|
1063
|
-
# control (currently just
|
|
1064
|
-
# @return [Hash, Object] the same
|
|
1063
|
+
# control (currently just `emailVerified`) never reach the wire.
|
|
1064
|
+
# @return [Hash, Object] the same `body` object, mutated.
|
|
1065
1065
|
def self.strip_server_controlled_keys!(body)
|
|
1066
1066
|
return body unless body.is_a?(Hash)
|
|
1067
1067
|
SERVER_CONTROLLED_KEYS.each do |k|
|
|
@@ -1072,10 +1072,10 @@ module Parse
|
|
|
1072
1072
|
end
|
|
1073
1073
|
|
|
1074
1074
|
# @!visibility private
|
|
1075
|
-
# Raises
|
|
1076
|
-
# attacker turn
|
|
1075
|
+
# Raises `ArgumentError` if `body` carries keys that would let an
|
|
1076
|
+
# attacker turn `Parse::User.create` into an account-takeover sink.
|
|
1077
1077
|
# Skipped when called through the SDK's federated-identity wrapper
|
|
1078
|
-
# ({.autologin_service}), which deliberately supplies
|
|
1078
|
+
# ({.autologin_service}), which deliberately supplies `authData` and
|
|
1079
1079
|
# is responsible for its provenance.
|
|
1080
1080
|
def self.assert_create_body_safe!(body)
|
|
1081
1081
|
return unless body.is_a?(Hash)
|
|
@@ -1102,7 +1102,7 @@ module Parse
|
|
|
1102
1102
|
# @see User.create
|
|
1103
1103
|
def self.autologin_service(service_name, auth_data, body: {})
|
|
1104
1104
|
# Trust-mark this call so {.assert_create_body_safe!} permits the
|
|
1105
|
-
#
|
|
1105
|
+
# `authData` that we are explicitly responsible for here. The
|
|
1106
1106
|
# marker is consumed inside {.create} before forwarding to the
|
|
1107
1107
|
# server.
|
|
1108
1108
|
body = body.merge({
|
|
@@ -1113,15 +1113,15 @@ module Parse
|
|
|
1113
1113
|
end
|
|
1114
1114
|
|
|
1115
1115
|
# Create and log in a new anonymous user via the
|
|
1116
|
-
#
|
|
1117
|
-
#
|
|
1116
|
+
# `authData.anonymous` provider. The returned user instance has a
|
|
1117
|
+
# `session_token` and an objectId, and {#anonymous?} returns true.
|
|
1118
1118
|
# Later, after the user has chosen a username and password, upgrade
|
|
1119
1119
|
# the account in-place with {#upgrade_anonymous!}.
|
|
1120
1120
|
#
|
|
1121
1121
|
# Parse Server requires the anonymous-provider payload to include a
|
|
1122
|
-
# client-generated
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1122
|
+
# client-generated `id`; this helper produces one via
|
|
1123
|
+
# `SecureRandom.uuid` so callers don't have to hand-roll the
|
|
1124
|
+
# `authData` shape.
|
|
1125
1125
|
#
|
|
1126
1126
|
# @return [User] a freshly-created, logged-in anonymous user.
|
|
1127
1127
|
# @see #upgrade_anonymous!
|
|
@@ -1176,7 +1176,7 @@ module Parse
|
|
|
1176
1176
|
case response.code
|
|
1177
1177
|
when Parse::Response::ERROR_EMAIL_NOT_FOUND
|
|
1178
1178
|
# Parse Server throws code 205 (EMAIL_NOT_FOUND) when
|
|
1179
|
-
#
|
|
1179
|
+
# `preventLoginWithUnverifiedEmail` is set and the account's email
|
|
1180
1180
|
# address has not yet been verified. Raise the typed error so callers
|
|
1181
1181
|
# can direct the user to verify their inbox without catching every
|
|
1182
1182
|
# AuthenticationError.
|
|
@@ -1398,24 +1398,24 @@ module Parse
|
|
|
1398
1398
|
|
|
1399
1399
|
# Verify this user's password without minting a session token.
|
|
1400
1400
|
#
|
|
1401
|
-
# Delegates to the
|
|
1402
|
-
# 7.1.0+) using this user's
|
|
1401
|
+
# Delegates to the `GET /parse/verifyPassword` endpoint (Parse Server
|
|
1402
|
+
# 7.1.0+) using this user's `username` and the supplied `password`. The
|
|
1403
1403
|
# check is purely credential validation — no session is created on
|
|
1404
1404
|
# success, and the user's existing sessions are unaffected.
|
|
1405
1405
|
#
|
|
1406
1406
|
# Use this as a step-up authentication gate: before allowing a sensitive
|
|
1407
1407
|
# action (e.g. changing an email address or deleting an account), call
|
|
1408
|
-
#
|
|
1408
|
+
# `verify_password` to confirm the caller still knows the password.
|
|
1409
1409
|
#
|
|
1410
1410
|
# @param password [String] the password to verify.
|
|
1411
|
-
# @return [Boolean]
|
|
1411
|
+
# @return [Boolean] `true` if the credentials are valid.
|
|
1412
1412
|
# @raise [Parse::Error::EmailNotVerifiedError] when the account exists but
|
|
1413
|
-
#
|
|
1413
|
+
# `preventLoginWithUnverifiedEmail` is enabled and the email has not been
|
|
1414
1414
|
# verified (Parse Server error code 205). The caller may want to prompt
|
|
1415
1415
|
# the user to check their inbox rather than treating this as a wrong-
|
|
1416
1416
|
# password failure.
|
|
1417
1417
|
# @raise [Parse::Error::AuthenticationError] when the username does not
|
|
1418
|
-
# exist or the password is wrong (code 101,
|
|
1418
|
+
# exist or the password is wrong (code 101, `OBJECT_NOT_FOUND`).
|
|
1419
1419
|
# @return [Boolean]
|
|
1420
1420
|
# @example
|
|
1421
1421
|
# # Step-up check before a destructive action
|
|
@@ -1472,7 +1472,7 @@ module Parse
|
|
|
1472
1472
|
# use `master: true` for the previous behavior.
|
|
1473
1473
|
#
|
|
1474
1474
|
# @param max_depth [Integer] maximum BFS depth (default: 10).
|
|
1475
|
-
# @param master [Boolean] when
|
|
1475
|
+
# @param master [Boolean] when `true`, bypass `_Role` CLP and run
|
|
1476
1476
|
# the role-graph lookup under master mode. Use for ACL-building
|
|
1477
1477
|
# code paths inside the SDK or in admin tooling.
|
|
1478
1478
|
# @param as [Parse::User, Parse::Pointer, nil] caller-scope. When
|
|
@@ -1480,7 +1480,7 @@ module Parse
|
|
|
1480
1480
|
# case). Pass a different user to ask "what would this caller
|
|
1481
1481
|
# see when introspecting this user's roles?"; the scope's
|
|
1482
1482
|
# permission set is checked against `_Role` CLP.
|
|
1483
|
-
# @return [Set<String>] role names (no
|
|
1483
|
+
# @return [Set<String>] role names (no `role:` prefix). Empty set
|
|
1484
1484
|
# when the user has no objectId yet or holds no roles.
|
|
1485
1485
|
# @raise [Parse::CLPScope::Denied] when the scope cannot `find`
|
|
1486
1486
|
# on `_Role` under the current CLP.
|
|
@@ -19,8 +19,8 @@ module Parse
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Namespace where
|
|
23
|
-
# generated
|
|
22
|
+
# Namespace where `Parse.auto_generate_models!` installs dynamically
|
|
23
|
+
# generated `Parse::Object` subclasses derived from server-side schema.
|
|
24
24
|
# Isolating them here prevents server-returned className strings from
|
|
25
25
|
# rebinding top-level constants like ::File, ::Logger, ::Process.
|
|
26
26
|
module Generated
|
|
@@ -39,9 +39,9 @@ module Parse
|
|
|
39
39
|
# Parse Server system classes that ship with the SDK as hand-written
|
|
40
40
|
# subclasses (Parse::User, Parse::Role, etc.). Schema-driven builds
|
|
41
41
|
# must NOT install additional fields or associations on these — a
|
|
42
|
-
# compromised Parse Server could otherwise inject an
|
|
43
|
-
# property onto the real
|
|
44
|
-
# accessor onto
|
|
42
|
+
# compromised Parse Server could otherwise inject an `is_admin`
|
|
43
|
+
# property onto the real `Parse::User` class, or a `password_history`
|
|
44
|
+
# accessor onto `_Session`, by returning a poisoned schema.
|
|
45
45
|
PROTECTED_SYSTEM_CLASSES = %w[
|
|
46
46
|
_User _Role _Session _Installation _Product _Audience _PushStatus
|
|
47
47
|
_JobStatus _JobSchedule _Hooks _GlobalConfig _SCHEMA _GraphQLConfig
|
|
@@ -123,8 +123,8 @@ module Parse
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# @!visibility private
|
|
126
|
-
# Validates a server-returned
|
|
127
|
-
# it to
|
|
126
|
+
# Validates a server-returned `targetClass` string before forwarding
|
|
127
|
+
# it to `belongs_to`/`has_many`. Returns `nil` for missing or
|
|
128
128
|
# invalid values so the association DSL falls back to its inferred
|
|
129
129
|
# default rather than installing an attacker-controlled class name
|
|
130
130
|
# (which could pivot a later type-confusion bypass).
|
|
@@ -78,7 +78,7 @@ module Parse
|
|
|
78
78
|
source: "Parse::CreateLock",
|
|
79
79
|
unavailable_error: Parse::CreateLockUnavailableError,
|
|
80
80
|
)
|
|
81
|
-
return LockBackend.
|
|
81
|
+
return LockBackend.synchronize_process_mutex(key, &block)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
owner = SecureRandom.uuid
|
|
@@ -708,13 +708,13 @@ module Parse
|
|
|
708
708
|
# writers when the caller is trusted.
|
|
709
709
|
# @param filter_protected [Boolean, nil] whether to filter out
|
|
710
710
|
# {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}. Defaults to
|
|
711
|
-
#
|
|
712
|
-
# can pass
|
|
711
|
+
# `dirty_track` for backwards-compat (the historical coupling). Callers
|
|
712
|
+
# can pass `true` explicitly to filter even on the trusted hydration
|
|
713
713
|
# path (used by {Parse::Object#initialize} when constructed with
|
|
714
|
-
#
|
|
714
|
+
# `trusted: false` but an `objectId` is in the hash). `false` explicitly
|
|
715
715
|
# preserves the legacy "server response" semantics.
|
|
716
716
|
# @param protected_set [Array<String>, nil] override which key list to
|
|
717
|
-
# filter when
|
|
717
|
+
# filter when `filter_protected` is true. Defaults to the wider
|
|
718
718
|
# {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}.
|
|
719
719
|
# {Parse::Object#initialize} passes
|
|
720
720
|
# {Parse::Properties::PROTECTED_INITIALIZE_KEYS} here to allow
|
data/lib/parse/model/file.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Parse
|
|
|
69
69
|
|
|
70
70
|
# @!visibility private
|
|
71
71
|
# Default cap on remote-fetched file size (50 MiB). Override via
|
|
72
|
-
#
|
|
72
|
+
# `Parse::File.max_remote_size`.
|
|
73
73
|
DEFAULT_MAX_REMOTE_SIZE = 50 * 1024 * 1024
|
|
74
74
|
# @!visibility private
|
|
75
75
|
# Default read/open timeout for remote fetches in seconds.
|
|
@@ -90,7 +90,7 @@ module Parse
|
|
|
90
90
|
].map { |c| IPAddr.new(c) }.freeze
|
|
91
91
|
# Restrictive port allowlist for Parse::File URL fetches. By default
|
|
92
92
|
# only the standard HTTP/HTTPS ports are permitted. Operators may
|
|
93
|
-
# extend
|
|
93
|
+
# extend `Parse::File.allowed_remote_ports` for legitimate non-standard
|
|
94
94
|
# CDN ports.
|
|
95
95
|
DEFAULT_ALLOWED_REMOTE_PORTS = [80, 443, 8080, 8443].freeze
|
|
96
96
|
# @return [String] the name of the file including extension (if any)
|
|
@@ -164,7 +164,7 @@ module Parse
|
|
|
164
164
|
end
|
|
165
165
|
|
|
166
166
|
# @return [Integer] Maximum byte size for a remote URL fetch via
|
|
167
|
-
#
|
|
167
|
+
# `Parse::File.create` / `Parse::File.new(url)`.
|
|
168
168
|
attr_writer :max_remote_size
|
|
169
169
|
def max_remote_size
|
|
170
170
|
@max_remote_size ||= DEFAULT_MAX_REMOTE_SIZE
|
|
@@ -526,16 +526,22 @@ module Parse
|
|
|
526
526
|
# the caller can read from.
|
|
527
527
|
#
|
|
528
528
|
# DNS rebinding mitigation: the host is resolved twice — once before
|
|
529
|
-
# the fetch and once via
|
|
530
|
-
# second-pass addresses are re-validated against
|
|
531
|
-
# any new private/internal IP causes an
|
|
529
|
+
# the fetch and once via `URI.open`'s underlying resolver. The
|
|
530
|
+
# second-pass addresses are re-validated against `BLOCKED_CIDRS`;
|
|
531
|
+
# any new private/internal IP causes an `ArgumentError` at progress
|
|
532
532
|
# time so the body cannot be streamed back. (Caveat: this is a
|
|
533
|
-
# best-effort defense — the TCP
|
|
533
|
+
# best-effort defense — the TCP `connect()` uses a third resolution
|
|
534
534
|
# that we cannot intercept without a custom socket factory. Operators
|
|
535
535
|
# who need strict guarantees should also enforce egress allowlists
|
|
536
536
|
# at the network layer.)
|
|
537
537
|
# @raise [ArgumentError] on any disallowed input or unsafe target.
|
|
538
|
-
def safe_open_url(url_string)
|
|
538
|
+
def safe_open_url(url_string, max_bytes: nil)
|
|
539
|
+
# Validate the per-call streaming cap up front. A non-positive value is
|
|
540
|
+
# a caller error, not a "cap everything" instruction: 0 would reject any
|
|
541
|
+
# non-empty body, and a negative cap makes `max_bytes < size_cap` always
|
|
542
|
+
# true, driving size_cap negative so every non-empty response raises
|
|
543
|
+
# "exceeds". Fail fast with a clear message before any DNS/host work.
|
|
544
|
+
max_bytes = coerce_positive_max_bytes(max_bytes)
|
|
539
545
|
uri = begin
|
|
540
546
|
URI.parse(url_string)
|
|
541
547
|
rescue URI::InvalidURIError => e
|
|
@@ -561,14 +567,22 @@ module Parse
|
|
|
561
567
|
end
|
|
562
568
|
resolved = assert_host_allowed!(host)
|
|
563
569
|
|
|
570
|
+
# The global cap always bounds the stream; a caller may pass a
|
|
571
|
+
# tighter `max_bytes:` to abort even earlier (e.g. image fetch with
|
|
572
|
+
# a per-request ceiling below Parse::File.max_remote_size). Only a
|
|
573
|
+
# smaller value takes effect — a caller cannot loosen the global cap.
|
|
574
|
+
# `max_bytes` is already a validated positive Integer (or nil) here.
|
|
575
|
+
# Only a value below the global cap takes effect — a caller cannot
|
|
576
|
+
# loosen the global ceiling.
|
|
564
577
|
size_cap = max_remote_size
|
|
578
|
+
size_cap = max_bytes if max_bytes && max_bytes < size_cap
|
|
565
579
|
timeout = remote_timeout
|
|
566
580
|
uri.open(read_timeout: timeout,
|
|
567
581
|
open_timeout: timeout,
|
|
568
582
|
redirect: false,
|
|
569
583
|
content_length_proc: ->(len) {
|
|
570
584
|
if len && len > size_cap
|
|
571
|
-
raise ArgumentError, "Remote file exceeds
|
|
585
|
+
raise ArgumentError, "Remote file exceeds the size cap (#{size_cap} bytes)"
|
|
572
586
|
end
|
|
573
587
|
# DNS-rebinding re-check: by the time content_length_proc
|
|
574
588
|
# fires, the connection has been established. Re-resolve
|
|
@@ -577,14 +591,33 @@ module Parse
|
|
|
577
591
|
},
|
|
578
592
|
progress_proc: ->(transferred) {
|
|
579
593
|
if transferred > size_cap
|
|
580
|
-
raise ArgumentError, "Remote file exceeds
|
|
594
|
+
raise ArgumentError, "Remote file exceeds the size cap (#{size_cap} bytes)"
|
|
581
595
|
end
|
|
582
596
|
})
|
|
583
597
|
end
|
|
584
598
|
|
|
585
599
|
# @!visibility private
|
|
586
|
-
#
|
|
587
|
-
#
|
|
600
|
+
# Coerce a caller-supplied `max_bytes:` to a positive Integer, or nil
|
|
601
|
+
# when unset. A non-numeric, zero, or negative value raises ArgumentError
|
|
602
|
+
# — the streaming cap is a positive byte ceiling, never a sentinel.
|
|
603
|
+
# @return [Integer, nil]
|
|
604
|
+
def coerce_positive_max_bytes(max_bytes)
|
|
605
|
+
return nil if max_bytes.nil?
|
|
606
|
+
requested =
|
|
607
|
+
begin
|
|
608
|
+
Integer(max_bytes)
|
|
609
|
+
rescue ArgumentError, TypeError
|
|
610
|
+
raise ArgumentError, "max_bytes must be a positive integer (got #{max_bytes.inspect})"
|
|
611
|
+
end
|
|
612
|
+
unless requested.positive?
|
|
613
|
+
raise ArgumentError, "max_bytes must be a positive integer (got #{requested})"
|
|
614
|
+
end
|
|
615
|
+
requested
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# @!visibility private
|
|
619
|
+
# Validates that `host` resolves only to public, non-blocked addresses.
|
|
620
|
+
# When `Parse::File.allowed_remote_hosts` is non-empty, host must also
|
|
588
621
|
# match an allowlist entry.
|
|
589
622
|
# @return [Array<IPAddr>] the addresses that passed validation.
|
|
590
623
|
def assert_host_allowed!(host)
|
|
@@ -614,10 +647,18 @@ module Parse
|
|
|
614
647
|
end
|
|
615
648
|
|
|
616
649
|
# @!visibility private
|
|
617
|
-
# DNS rebinding re-check.
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
#
|
|
650
|
+
# DNS rebinding re-check, best-effort. After the connection is
|
|
651
|
+
# established, re-resolves `host` and refuses if any currently-resolved
|
|
652
|
+
# address is private/internal (in BLOCKED_CIDRS) — the SSRF shape where a
|
|
653
|
+
# host that first resolved to a public address later rebinds to an
|
|
654
|
+
# internal one. It deliberately does NOT require the re-resolved set to
|
|
655
|
+
# equal the first resolution (`prior_addrs`): legitimate round-robin / CDN
|
|
656
|
+
# hosts return different public IPs per lookup, and the HTTP client
|
|
657
|
+
# resolves the actual socket itself, so pinning to the first IPs here
|
|
658
|
+
# would add false positives without binding the real connection.
|
|
659
|
+
# `prior_addrs` only gates the check to the case where the initial
|
|
660
|
+
# resolution succeeded. Kernel resolver caches and a resolution at
|
|
661
|
+
# connect-time proper are out of scope.
|
|
621
662
|
def assert_host_not_rebound!(host, prior_addrs)
|
|
622
663
|
return if prior_addrs.nil? || prior_addrs.empty?
|
|
623
664
|
current = resolve_addresses(host)
|
data/lib/parse/model/model.rb
CHANGED
|
@@ -81,15 +81,15 @@ module Parse
|
|
|
81
81
|
# leading-underscore storage form. Consulted by {String#to_parse_class}
|
|
82
82
|
# ONLY as a fallback when {find_class} cannot resolve the name — which
|
|
83
83
|
# happens at class-declaration time for a built-in whose Ruby class is not
|
|
84
|
-
# yet registered (e.g.
|
|
85
|
-
# before
|
|
86
|
-
# froze the wrong literal (
|
|
87
|
-
# and pushed it to the server schema as the pointer
|
|
88
|
-
# Parse Server then rejected (
|
|
84
|
+
# yet registered (e.g. `Parse::Installation` declares `belongs_to :user`
|
|
85
|
+
# before `Parse::User` is loaded). Without this fallback the conversion
|
|
86
|
+
# froze the wrong literal (`"User"`) into the association `references` map
|
|
87
|
+
# and pushed it to the server schema as the pointer `targetClass`, which
|
|
88
|
+
# Parse Server then rejected (`Pointer<User>` vs `Pointer<_User>`). A
|
|
89
89
|
# genuinely-registered class still wins via {find_class}, so a custom
|
|
90
|
-
#
|
|
90
|
+
# `parse_class` table mapping is never overridden by this map.
|
|
91
91
|
# Keys are the camelized bare names (the form {String#to_parse_class}
|
|
92
|
-
# computes before lookup); only
|
|
92
|
+
# computes before lookup); only `User` is actually targeted by a built-in
|
|
93
93
|
# association before its class registers, the rest are hygiene so an app
|
|
94
94
|
# that declares a pointer/relation to any built-in resolves correctly
|
|
95
95
|
# regardless of load order.
|
|
@@ -235,27 +235,27 @@ module Parse
|
|
|
235
235
|
# Whether two Parse class-name strings denote the same class. This is the
|
|
236
236
|
# canonical equality used to suppress spurious className-mismatch warnings.
|
|
237
237
|
# Two names are the same class when they are string-equal, when one is the
|
|
238
|
-
# leading-underscore system form of the other (
|
|
239
|
-
# <->
|
|
240
|
-
#
|
|
241
|
-
# subclass (covers custom
|
|
238
|
+
# leading-underscore system form of the other (`User` <-> `_User`, `Role`
|
|
239
|
+
# <-> `_Role`, `Installation` <-> `_Installation`, `Session` <->
|
|
240
|
+
# `_Session`), or when both resolve to the same registered `Parse::Object`
|
|
241
|
+
# subclass (covers custom `parse_class` table mappings).
|
|
242
242
|
#
|
|
243
243
|
# The underscore rule is what makes the comparison correct independent of
|
|
244
244
|
# autoload order — at declaration time {find_class} may not yet have
|
|
245
|
-
#
|
|
246
|
-
# rather than
|
|
245
|
+
# `Parse::User` registered (so a `belongs_to :user` captures `"User"`
|
|
246
|
+
# rather than `"_User"`), but the server always emits the `_User` storage
|
|
247
247
|
# form, and both denote the same class. The rule matches exactly one
|
|
248
|
-
# system prefix underscore, so a malformed
|
|
249
|
-
# with
|
|
248
|
+
# system prefix underscore, so a malformed `"__User"` is not conflated
|
|
249
|
+
# with `"_User"`.
|
|
250
250
|
#
|
|
251
251
|
# These warnings are an ADVISORY type-confusion signal, not the
|
|
252
252
|
# enforcement mechanism: every call site that consults this method builds
|
|
253
253
|
# the resulting object from the declared/trusted class regardless of the
|
|
254
|
-
# incoming className (see
|
|
255
|
-
#
|
|
254
|
+
# incoming className (see `Parse::Object.build`, which always uses the
|
|
255
|
+
# `table` argument the caller passes). A false-positive here can therefore
|
|
256
256
|
# only suppress a log line — it can never route a pointer of the wrong
|
|
257
|
-
# class into a typed slot. Distinct classes still compare unequal (
|
|
258
|
-
# vs
|
|
257
|
+
# class into a typed slot. Distinct classes still compare unequal (`User`
|
|
258
|
+
# vs `_Session`, `User` vs `_Role`, and `nil`), so a genuinely mismatched
|
|
259
259
|
# pointer still surfaces in logs.
|
|
260
260
|
#
|
|
261
261
|
# @param a [String, Symbol, nil] a Parse class name.
|