parse-stack-next 4.5.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 +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Note: Do not require "../object" here - this file is loaded from object.rb
|
|
5
|
+
# and adding that require would create a circular dependency.
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
class Error
|
|
9
|
+
# 200 Error code indicating that the username is missing or empty.
|
|
10
|
+
class UsernameMissingError < Error; end
|
|
11
|
+
|
|
12
|
+
# 201 Error code indicating that the password is missing or empty.
|
|
13
|
+
class PasswordMissingError < Error; end
|
|
14
|
+
|
|
15
|
+
# Error code 202: indicating that the username has already been taken.
|
|
16
|
+
class UsernameTakenError < Error; end
|
|
17
|
+
|
|
18
|
+
# 203 Error code indicating that the email has already been taken.
|
|
19
|
+
class EmailTakenError < Error; end
|
|
20
|
+
|
|
21
|
+
# 204 Error code indicating that the email is missing, but must be specified.
|
|
22
|
+
class EmailMissing < Error; end
|
|
23
|
+
|
|
24
|
+
# 205 Error code indicating that a user with the specified email was not found.
|
|
25
|
+
class EmailNotFound < Error; end
|
|
26
|
+
|
|
27
|
+
# 125 Error code indicating that the email address was invalid.
|
|
28
|
+
class InvalidEmailAddress < Error; end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The main class representing the _User table in Parse. A user can either be signed up or anonymous.
|
|
32
|
+
# All users need to have a username and a password, with email being optional but globally unique if set.
|
|
33
|
+
# You may add additional properties by redeclaring the class to match your specific schema.
|
|
34
|
+
#
|
|
35
|
+
# The default schema for the {User} class is as follows:
|
|
36
|
+
#
|
|
37
|
+
# class Parse::User < Parse::Object
|
|
38
|
+
# # See Parse::Object for inherited properties...
|
|
39
|
+
#
|
|
40
|
+
# property :auth_data, :object
|
|
41
|
+
# property :username
|
|
42
|
+
# property :password
|
|
43
|
+
# property :email
|
|
44
|
+
#
|
|
45
|
+
# has_many :active_sessions, as: :session
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# *Signup*
|
|
49
|
+
#
|
|
50
|
+
# You can signup new users in two ways. You can either use a class method
|
|
51
|
+
# {Parse::User.signup} to create a new user with the minimum fields of username,
|
|
52
|
+
# password and email, or create a {Parse::User} object can call the {#signup!}
|
|
53
|
+
# method. If signup fails, it will raise the corresponding exception.
|
|
54
|
+
#
|
|
55
|
+
# user = Parse::User.signup(username, password, email)
|
|
56
|
+
#
|
|
57
|
+
# #or
|
|
58
|
+
# user = Parse::User.new username: "user", password: "s3cret"
|
|
59
|
+
# user.signup!
|
|
60
|
+
#
|
|
61
|
+
# *Login/Logout*
|
|
62
|
+
#
|
|
63
|
+
# With the {Parse::User} class, you can also perform login and logout
|
|
64
|
+
# functionality. The class special accessors for {#session_token} and {#session}
|
|
65
|
+
# to manage its authentication state. This will allow you to authenticate
|
|
66
|
+
# users as well as perform Parse queries as a specific user using their session
|
|
67
|
+
# token. To login a user, use the {Parse::User.login} method by supplying the
|
|
68
|
+
# corresponding username and password, or if you already have a user record,
|
|
69
|
+
# use {#login!} with the proper password.
|
|
70
|
+
#
|
|
71
|
+
# user = Parse::User.login(username,password)
|
|
72
|
+
# user.session_token # session token from a Parse::Session
|
|
73
|
+
# user.session # Parse::Session tied to the token
|
|
74
|
+
#
|
|
75
|
+
# # You can login user records
|
|
76
|
+
# user = Parse::User.first
|
|
77
|
+
# user.session_token # nil
|
|
78
|
+
#
|
|
79
|
+
# passwd = 'p_n7!-e8' # corresponding password
|
|
80
|
+
# user.login!(passwd) # true
|
|
81
|
+
#
|
|
82
|
+
# user.session_token # 'r:pnktnjyb996sj4p156gjtp4im'
|
|
83
|
+
#
|
|
84
|
+
# # logout to delete the session
|
|
85
|
+
# user.logout
|
|
86
|
+
#
|
|
87
|
+
# If you happen to already have a valid session token, you can use it to
|
|
88
|
+
# retrieve the corresponding Parse::User.
|
|
89
|
+
#
|
|
90
|
+
# # finds user with session token
|
|
91
|
+
# user = Parse::User.session(session_token)
|
|
92
|
+
#
|
|
93
|
+
# user.logout # deletes the corresponding session
|
|
94
|
+
#
|
|
95
|
+
# *OAuth-Login*
|
|
96
|
+
#
|
|
97
|
+
# You can signup users using third-party services like Facebook and Twitter as
|
|
98
|
+
# described in {http://docs.parseplatform.org/rest/guide/#signing-up
|
|
99
|
+
# Signing Up and Logging In}. To do this with Parse-Stack, you can call the
|
|
100
|
+
# {Parse::User.autologin_service} method by passing the service name and the
|
|
101
|
+
# corresponding authentication hash data. For a listing of supported third-party
|
|
102
|
+
# authentication services, see {http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication OAuth}.
|
|
103
|
+
#
|
|
104
|
+
# fb_auth = {}
|
|
105
|
+
# fb_auth[:id] = "123456789"
|
|
106
|
+
# fb_auth[:access_token] = "SaMpLeAAiZBLR995wxBvSGNoTrEaL"
|
|
107
|
+
# fb_auth[:expiration_date] = "2025-02-21T23:49:36.353Z"
|
|
108
|
+
#
|
|
109
|
+
# # signup or login a user with this auth data.
|
|
110
|
+
# user = Parse::User.autologin_service(:facebook, fb_auth)
|
|
111
|
+
#
|
|
112
|
+
# You may also combine both approaches of signing up a new user with a
|
|
113
|
+
# third-party service and set additional custom fields. For this, use the
|
|
114
|
+
# method {Parse::User.create}.
|
|
115
|
+
#
|
|
116
|
+
# # or to signup a user with additional data, but linked to Facebook
|
|
117
|
+
# data = {
|
|
118
|
+
# username: "johnsmith",
|
|
119
|
+
# name: "John",
|
|
120
|
+
# email: "user@example.com",
|
|
121
|
+
# authData: { facebook: fb_auth }
|
|
122
|
+
# }
|
|
123
|
+
# user = Parse::User.create data
|
|
124
|
+
#
|
|
125
|
+
# *Linking/Unlinking*
|
|
126
|
+
#
|
|
127
|
+
# You can link or unlink user accounts with third-party services like
|
|
128
|
+
# Facebook and Twitter as described in:
|
|
129
|
+
# {http://docs.parseplatform.org/rest/guide/#linking-users Linking and Unlinking Users}.
|
|
130
|
+
# To do this, you must first get the corresponding authentication data for the
|
|
131
|
+
# specific service, and then apply it to the user using the linking and
|
|
132
|
+
# unlinking methods. Each method returns true or false if the action was
|
|
133
|
+
# successful. For a listing of supported third-party authentication services,
|
|
134
|
+
# see {http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication OAuth}.
|
|
135
|
+
#
|
|
136
|
+
# user = Parse::User.first
|
|
137
|
+
#
|
|
138
|
+
# fb_auth = { ... } # Facebook auth data
|
|
139
|
+
#
|
|
140
|
+
# # Link this user's Facebook account with Parse
|
|
141
|
+
# user.link_auth_data! :facebook, fb_auth
|
|
142
|
+
#
|
|
143
|
+
# # Unlinks this user's Facebook account from Parse
|
|
144
|
+
# user.unlink_auth_data! :facebook
|
|
145
|
+
#
|
|
146
|
+
# @see Parse::Object
|
|
147
|
+
class User < Parse::Object
|
|
148
|
+
parse_class Parse::Model::CLASS_USER
|
|
149
|
+
|
|
150
|
+
# When true (default), saving a new {Parse::User} that has a `password`
|
|
151
|
+
# value routes through Parse Server's signup endpoint (`POST /parse/users`)
|
|
152
|
+
# with the `X-Parse-Revocable-Session` header set, so the signup response
|
|
153
|
+
# returns a session token that is applied to the in-memory user object
|
|
154
|
+
# via the standard `sessionToken_set_attribute!` hydration path. Without
|
|
155
|
+
# this flag, `Parse::User.new(...).save!` left `session_token` `nil`
|
|
156
|
+
# because the underlying create path did not request a revocable session.
|
|
157
|
+
#
|
|
158
|
+
# Set to `false` to always create users without requesting a revocable
|
|
159
|
+
# session token - for example, when a master-key server-side script is
|
|
160
|
+
# provisioning user rows that will receive credentials later. New users
|
|
161
|
+
# created with no password always fall through to the standard create
|
|
162
|
+
# path regardless of this flag.
|
|
163
|
+
#
|
|
164
|
+
# `auth_data` (federated identity / OAuth) signup is deliberately NOT
|
|
165
|
+
# triggered by this flag. `POST /parse/users` treats `auth_data` as a
|
|
166
|
+
# claim against an existing account, so allowing mass-assigned `auth_data`
|
|
167
|
+
# to trigger a revocable-session signup would let attacker-controlled
|
|
168
|
+
# params plant another user's session token onto the in-memory object.
|
|
169
|
+
# Use {.autologin_service} or {.signup} (the explicit class methods) for
|
|
170
|
+
# OAuth-driven signup; both bypass the mass-assignment filter because the
|
|
171
|
+
# caller is explicitly choosing the federated-identity flow.
|
|
172
|
+
#
|
|
173
|
+
# Inherited through subclasses via {ActiveSupport::Concern}'s
|
|
174
|
+
# `class_attribute`, so an application-specific subclass may override
|
|
175
|
+
# the default without affecting `Parse::User` itself.
|
|
176
|
+
#
|
|
177
|
+
# @return [Boolean]
|
|
178
|
+
class_attribute :signup_on_save, instance_writer: false
|
|
179
|
+
self.signup_on_save = true
|
|
180
|
+
|
|
181
|
+
# @return [String] The session token if this user is logged in.
|
|
182
|
+
attr_reader :session_token
|
|
183
|
+
|
|
184
|
+
# @!attribute auth_data
|
|
185
|
+
# The auth data for this Parse::User. Depending on how this user is authenticated or
|
|
186
|
+
# logged in, the contents may be different, especially if you are using another third-party
|
|
187
|
+
# authentication mechanism like Facebook/Twitter.
|
|
188
|
+
# @return [Hash] Auth data hash object.
|
|
189
|
+
property :auth_data, :object
|
|
190
|
+
|
|
191
|
+
# @!attribute email
|
|
192
|
+
# Emails are optional in Parse, but if set, they must be unique.
|
|
193
|
+
# @return [String] The email field.
|
|
194
|
+
property :email
|
|
195
|
+
|
|
196
|
+
# @overload password=(value)
|
|
197
|
+
# You may set a password for this user when you are creating them. Parse never returns a
|
|
198
|
+
# Parse::User's password when a record is fetched. Therefore, normally this getter is nil.
|
|
199
|
+
# While this API exists, it is recommended you use either the #login! or #signup! methods.
|
|
200
|
+
# (see #login!)
|
|
201
|
+
# @return [String] The password you set.
|
|
202
|
+
property :password
|
|
203
|
+
|
|
204
|
+
# @!attribute username
|
|
205
|
+
# All Parse users have a username and must be globally unique.
|
|
206
|
+
# @return [String] The user's username.
|
|
207
|
+
property :username
|
|
208
|
+
|
|
209
|
+
# @!attribute email_verified
|
|
210
|
+
# Whether this user's email address has been verified. Set by Parse Server
|
|
211
|
+
# when the user follows the verification link delivered by the email
|
|
212
|
+
# adapter, and applied to the in-memory object by {#signup!} / signup-on-save
|
|
213
|
+
# when the server includes it in the signup response (see
|
|
214
|
+
# +SIGNUP_RESPONSE_APPLY_KEYS+).
|
|
215
|
+
# @return [Boolean]
|
|
216
|
+
property :email_verified, :boolean
|
|
217
|
+
|
|
218
|
+
# @!attribute active_sessions
|
|
219
|
+
# A has_many relationship to all {Parse::Session} instances for this user. This
|
|
220
|
+
# will query the _Session collection for all sessions which have this user in it's `user`
|
|
221
|
+
# column.
|
|
222
|
+
# @version 1.7.1
|
|
223
|
+
# @return [Array<Parse::Session>] A list of active Parse::Session objects.
|
|
224
|
+
has_many :active_sessions, as: :session
|
|
225
|
+
|
|
226
|
+
# CHANGE -- ACLs can be managed
|
|
227
|
+
# before_save do
|
|
228
|
+
# # You cannot specify user ACLs.
|
|
229
|
+
# self.clear_attribute_change!([:acl])
|
|
230
|
+
# end
|
|
231
|
+
|
|
232
|
+
# `emailVerified` is server-controlled: Parse Server flips it when the
|
|
233
|
+
# user follows the verification link, and only master-key callers (e.g.
|
|
234
|
+
# a `beforeSignUp` cloud function approving an internal email domain)
|
|
235
|
+
# are meant to set it explicitly. Client writes from any platform —
|
|
236
|
+
# this Ruby SDK, iOS, JS, etc. — are silently reverted at the
|
|
237
|
+
# `_User.beforeSave` webhook boundary.
|
|
238
|
+
#
|
|
239
|
+
# This complements the SDK-side {SERVER_CONTROLLED_KEYS} strip
|
|
240
|
+
# ({strip_server_controlled_keys!}), which removes the field from
|
|
241
|
+
# outbound signup/create bodies before the request leaves the SDK.
|
|
242
|
+
# The guard is the cross-client backstop and only runs when the
|
|
243
|
+
# deployment has the Parse Server webhook callback wired to a Ruby app
|
|
244
|
+
# running the `Parse::Webhooks` middleware. Reads are unaffected — a
|
|
245
|
+
# logged-in user can still see their own `email_verified` flag.
|
|
246
|
+
guard :email_verified, :master_only
|
|
247
|
+
|
|
248
|
+
# @return [Boolean] true if this user is anonymous (i.e. created
|
|
249
|
+
# via the +authData.anonymous+ provider rather than via signup
|
|
250
|
+
# with a username/password or a real OAuth provider).
|
|
251
|
+
def anonymous?
|
|
252
|
+
!anonymous_id.nil?
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the anonymous identifier only if this user is anonymous.
|
|
256
|
+
# @see #anonymous?
|
|
257
|
+
# @return [String] The anonymous identifier for this anonymous user.
|
|
258
|
+
def anonymous_id
|
|
259
|
+
auth_data["anonymous"]["id"] if auth_data.present? && auth_data["anonymous"].is_a?(Hash)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Adds the third-party authentication data to for a given service.
|
|
263
|
+
# @param service_name [Symbol] The name of the service (ex. :facebook)
|
|
264
|
+
# @param data [Hash] The body of the OAuth data. Dependent on each service.
|
|
265
|
+
# @raise [Parse::Client::ResponseError] If user was not successfully linked
|
|
266
|
+
def link_auth_data!(service_name, **data)
|
|
267
|
+
response = client.set_service_auth_data(id, service_name, data)
|
|
268
|
+
raise Parse::Client::ResponseError, response if response.error?
|
|
269
|
+
apply_attributes!(response.result)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Removes third-party authentication data for this user
|
|
273
|
+
# @param service_name [Symbol] The name of the third-party service (ex. :facebook)
|
|
274
|
+
# @raise [Parse::Client::ResponseError] If user was not successfully unlinked
|
|
275
|
+
# @return [Boolean] True/false if successful.
|
|
276
|
+
def unlink_auth_data!(service_name)
|
|
277
|
+
response = client.set_service_auth_data(id, service_name, nil)
|
|
278
|
+
raise Parse::Client::ResponseError, response if response.error?
|
|
279
|
+
apply_attributes!(response.result)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# @!visibility private
|
|
283
|
+
# So that apply_attributes! works with session_token for login
|
|
284
|
+
def session_token_set_attribute!(token, track = false)
|
|
285
|
+
@session_token = token.to_s
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
alias_method :sessionToken_set_attribute!, :session_token_set_attribute!
|
|
289
|
+
|
|
290
|
+
# @return [Boolean] true if this user has a session token.
|
|
291
|
+
def logged_in?
|
|
292
|
+
self.session_token.present?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Request a password reset for this user
|
|
296
|
+
# @return [Boolean] true if it was successful requested. false otherwise.
|
|
297
|
+
# @see Parse::User.request_password_reset
|
|
298
|
+
def request_password_reset
|
|
299
|
+
return false if email.nil?
|
|
300
|
+
Parse::User.request_password_reset(email)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# You may set a password for this user when you are creating them. Parse never returns a
|
|
304
|
+
# @param passwd The user's password to be used for signing up.
|
|
305
|
+
# @raise [Parse::Error::UsernameMissingError] If username is missing.
|
|
306
|
+
# @raise [Parse::Error::PasswordMissingError] If password is missing.
|
|
307
|
+
# @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
|
|
308
|
+
# @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
|
|
309
|
+
# @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
|
|
310
|
+
# @raise [Parse::Client::ResponseError] An unknown error occurred.
|
|
311
|
+
# @return [Boolean] True if signup it was successful. If it fails an exception is thrown.
|
|
312
|
+
def signup!(passwd = nil)
|
|
313
|
+
self.password = passwd || password
|
|
314
|
+
if username.blank?
|
|
315
|
+
raise Parse::Error::UsernameMissingError, "Signup requires a username."
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
if password.blank?
|
|
319
|
+
raise Parse::Error::PasswordMissingError, "Signup requires a password."
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
signup_attrs = attribute_updates
|
|
323
|
+
# See {#signup_create} for the rationale on the safe-pattern check.
|
|
324
|
+
if self.class.signup_body_self_only_acl_safe?(signup_attrs)
|
|
325
|
+
signup_attrs.except!(:createdAt, :updatedAt, "createdAt", "updatedAt")
|
|
326
|
+
else
|
|
327
|
+
signup_attrs.except!(*Parse::Properties::BASE_FIELD_MAP.flatten)
|
|
328
|
+
end
|
|
329
|
+
self.class.strip_server_controlled_keys!(signup_attrs)
|
|
330
|
+
|
|
331
|
+
# first signup the user, then save any additional attributes
|
|
332
|
+
response = client.create_user signup_attrs
|
|
333
|
+
|
|
334
|
+
if response.success?
|
|
335
|
+
# Restrict what the server can plant into the in-memory user via
|
|
336
|
+
# the signup response, matching the defense in {#signup_create}.
|
|
337
|
+
# `POST /parse/users` legitimately returns objectId, createdAt,
|
|
338
|
+
# updatedAt (extracted into @-vars directly below), sessionToken,
|
|
339
|
+
# and emailVerified. Any other key in the response body --
|
|
340
|
+
# `authData`, `_rperm`, `_wperm`, `roles`, etc. -- is dropped, so
|
|
341
|
+
# a compromised or MITM'd Parse Server cannot use this code path
|
|
342
|
+
# to plant credentials/permissions onto the user we just signed
|
|
343
|
+
# up. The previous `apply_attributes! response.result` accepted
|
|
344
|
+
# every key the server returned through the typed property
|
|
345
|
+
# writers (`authData_set_attribute!` exists because we declare
|
|
346
|
+
# `property :auth_data, :object`), which was a footgun the
|
|
347
|
+
# save-as-signup path had already addressed.
|
|
348
|
+
result = response.result
|
|
349
|
+
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
350
|
+
@created_at = result["createdAt"] || @created_at
|
|
351
|
+
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
352
|
+
set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
|
|
353
|
+
# Drop the plaintext password from memory now that the server
|
|
354
|
+
# has it hashed and we no longer need it. Matches the Parse JS
|
|
355
|
+
# SDK behavior of clearing the password attribute after a
|
|
356
|
+
# successful save/signup. Uses direct ivar assignment so the
|
|
357
|
+
# dirty tracker doesn't record this clear as a pending change
|
|
358
|
+
# that would be re-sent on the next save.
|
|
359
|
+
@password = nil
|
|
360
|
+
# Mirror Parse::Object#save: a successful round-trip means the
|
|
361
|
+
# locally-set credential fields are now in sync with the server
|
|
362
|
+
# and must NOT be re-sent on the next save. Without this, a
|
|
363
|
+
# subsequent user.save! re-transmits `password`, which Parse
|
|
364
|
+
# Server treats as a password change under
|
|
365
|
+
# revokeSessionOnPasswordReset and revokes the session just
|
|
366
|
+
# minted by this signup.
|
|
367
|
+
changes_applied!
|
|
368
|
+
clear_partial_fetch_state!
|
|
369
|
+
return true
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
case response.code
|
|
373
|
+
when Parse::Response::ERROR_USERNAME_MISSING
|
|
374
|
+
raise Parse::Error::UsernameMissingError, response
|
|
375
|
+
when Parse::Response::ERROR_PASSWORD_MISSING
|
|
376
|
+
raise Parse::Error::PasswordMissingError, response
|
|
377
|
+
when Parse::Response::ERROR_USERNAME_TAKEN
|
|
378
|
+
raise Parse::Error::UsernameTakenError, response
|
|
379
|
+
when Parse::Response::ERROR_EMAIL_TAKEN
|
|
380
|
+
raise Parse::Error::EmailTakenError, response
|
|
381
|
+
when Parse::Response::ERROR_EMAIL_INVALID
|
|
382
|
+
raise Parse::Error::InvalidEmailAddress, response
|
|
383
|
+
end
|
|
384
|
+
raise Parse::Client::ResponseError, response
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Override of {Parse::Core::Actions::InstanceMethods#create} so that
|
|
388
|
+
# saving a new user that has a `password` goes through Parse Server's
|
|
389
|
+
# signup endpoint and the returned session token is applied to the
|
|
390
|
+
# in-memory object. Falls through to the inherited raw `_User` insert
|
|
391
|
+
# when the new user has no password or when {.signup_on_save} has been
|
|
392
|
+
# disabled. Like the inherited `:create` path, the `before_create` /
|
|
393
|
+
# `after_create` callback chain still fires and the method returns the
|
|
394
|
+
# response's success flag (errors propagate to {Parse::Object#save} as
|
|
395
|
+
# a `false` return, which the caller may turn into a
|
|
396
|
+
# {Parse::RecordNotSaved} via `save!` / `autoraise: true`).
|
|
397
|
+
#
|
|
398
|
+
# `auth_data`-only signups (federated-identity / OAuth flows where no
|
|
399
|
+
# password is set) are deliberately NOT routed through this path,
|
|
400
|
+
# because `POST /parse/users` treats `auth_data` as an identity claim
|
|
401
|
+
# against an existing user — accepting it from a mass-assigned hash
|
|
402
|
+
# would expose a session-token planting vector. OAuth signup is the
|
|
403
|
+
# responsibility of the explicit {#signup!} method (or
|
|
404
|
+
# {Parse::User.autologin_service}), whose call sites necessarily make
|
|
405
|
+
# the federated-identity decision themselves.
|
|
406
|
+
# @!visibility private
|
|
407
|
+
def create
|
|
408
|
+
if self.class.signup_on_save && self.password.present?
|
|
409
|
+
signup_create
|
|
410
|
+
else
|
|
411
|
+
super
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Login and get a session token for this user.
|
|
416
|
+
# @param passwd [String] The password for this user.
|
|
417
|
+
# @return [Boolean] True/false if we received a valid session token.
|
|
418
|
+
def login!(passwd = nil)
|
|
419
|
+
self.password = passwd || self.password
|
|
420
|
+
response = client.login(username.to_s, password.to_s)
|
|
421
|
+
if response.success?
|
|
422
|
+
# Unlike signup, login's response is the canonical state of an
|
|
423
|
+
# existing user, including any linked authData. Applying the
|
|
424
|
+
# full response body here is intentional -- the server is
|
|
425
|
+
# telling us what the account currently looks like. (Compare
|
|
426
|
+
# signup, where we narrow to an allow-list because a brand-new
|
|
427
|
+
# account has no legitimate authData to report.)
|
|
428
|
+
apply_attributes! response.result
|
|
429
|
+
# Drop the plaintext password from memory now that the login
|
|
430
|
+
# has succeeded. Direct ivar assignment so the dirty tracker
|
|
431
|
+
# doesn't record this clear as a pending change.
|
|
432
|
+
@password = nil
|
|
433
|
+
# Clear dirty state so a subsequent user.save! does not re-send
|
|
434
|
+
# `password` (which Parse Server would treat as a password
|
|
435
|
+
# change and use to revoke the session this login just issued).
|
|
436
|
+
# See the matching note in #signup!.
|
|
437
|
+
changes_applied!
|
|
438
|
+
clear_partial_fetch_state!
|
|
439
|
+
end
|
|
440
|
+
self.session_token.present?
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Invalid the current session token for this logged in user.
|
|
444
|
+
# @return [Boolean] True/false if successful
|
|
445
|
+
def logout
|
|
446
|
+
return true if self.session_token.blank?
|
|
447
|
+
client.logout session_token
|
|
448
|
+
self.session_token = nil
|
|
449
|
+
true
|
|
450
|
+
rescue
|
|
451
|
+
false
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# @!visibility private
|
|
455
|
+
def session_token=(token)
|
|
456
|
+
@session = nil
|
|
457
|
+
@session_token = token
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# @return [Session] the session corresponding to the user's session token.
|
|
461
|
+
def session
|
|
462
|
+
if @session.blank? && @session_token.present?
|
|
463
|
+
response = client.fetch_session(@session_token)
|
|
464
|
+
# Trusted hydration: +response.result+ is the server-side
|
|
465
|
+
# _Session row, which legitimately includes +sessionToken+,
|
|
466
|
+
# +createdAt+, +updatedAt+, and other protected keys. Route
|
|
467
|
+
# through {Parse::Object.build} which handles the trusted-init
|
|
468
|
+
# signalling.
|
|
469
|
+
@session ||= Parse::Object.build(response.result, Parse::Model::CLASS_SESSION)
|
|
470
|
+
end
|
|
471
|
+
@session
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# @!visibility private
|
|
475
|
+
# Keys that must never flow through +Parse::User.create+ from a
|
|
476
|
+
# mass-assigned hash. +authData+ on the user-signup endpoint causes
|
|
477
|
+
# Parse Server to silently log into the existing account that matches
|
|
478
|
+
# that auth_data and return ITS sessionToken — full account takeover
|
|
479
|
+
# if the caller blindly forwards client-supplied parameters.
|
|
480
|
+
# +objectId+ allows the caller to pick the user's identifier on
|
|
481
|
+
# creation, sometimes targetable depending on Parse Server config.
|
|
482
|
+
UNSAFE_CREATE_KEYS = %i[authData auth_data objectId id].freeze
|
|
483
|
+
|
|
484
|
+
# @!visibility private
|
|
485
|
+
# Fields that are server-controlled and must be stripped from any body
|
|
486
|
+
# that the SDK sends to the signup endpoint or +Parse::User.create+,
|
|
487
|
+
# regardless of who supplied them. Unlike {UNSAFE_CREATE_KEYS}, passing
|
|
488
|
+
# one of these is not refused (no exception is raised); the field is
|
|
489
|
+
# silently dropped before wire transit.
|
|
490
|
+
#
|
|
491
|
+
# +emailVerified+ is the canonical case: Parse Server's default `_User`
|
|
492
|
+
# CLP restricts writes to the master key, so a caller-supplied value
|
|
493
|
+
# would normally be rejected anyway — but the SDK strips it as
|
|
494
|
+
# defense-in-depth so signup with mass-assigned attributes cannot
|
|
495
|
+
# smuggle a verified=true onto a brand-new account if the deployment
|
|
496
|
+
# has loosened the default CLP. (Update-path coverage is handled by
|
|
497
|
+
# the {Parse::Core::FieldGuards} declaration
|
|
498
|
+
# {guard :email_verified, :master_only} below, which silently reverts
|
|
499
|
+
# client writes at the `_User.beforeSave` webhook boundary.)
|
|
500
|
+
#
|
|
501
|
+
# The underscore-prefixed entries are internal Parse Server `_User`
|
|
502
|
+
# bookkeeping columns (verify tokens, perishable tokens, the bcrypt
|
|
503
|
+
# password hash, lockout state, etc.). Parse Server rejects writes to
|
|
504
|
+
# them from non-master callers anyway, but the SDK strips them as a
|
|
505
|
+
# belt-and-suspenders measure so a mass-assigned hash from request
|
|
506
|
+
# parameters cannot reach the wire with these keys at all.
|
|
507
|
+
#
|
|
508
|
+
# The trusted signup-response apply path ({SIGNUP_RESPONSE_APPLY_KEYS})
|
|
509
|
+
# is unaffected by this strip because it uses {#set_attributes!}, not
|
|
510
|
+
# the dirty-tracked setter that {#attribute_updates} reads from.
|
|
511
|
+
SERVER_CONTROLLED_KEYS = %i[
|
|
512
|
+
emailVerified email_verified
|
|
513
|
+
_hashed_password
|
|
514
|
+
_email_verify_token _email_verify_token_expires_at
|
|
515
|
+
_perishable_token _perishable_token_expires_at
|
|
516
|
+
_password_history
|
|
517
|
+
_failed_login_count
|
|
518
|
+
_account_lockout_expires_at
|
|
519
|
+
].freeze
|
|
520
|
+
|
|
521
|
+
# Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.
|
|
522
|
+
#
|
|
523
|
+
# Mass-assignment of +authData+/+auth_data+/+objectId+ is refused. If you
|
|
524
|
+
# intend to create-or-login a user via federated identity, use
|
|
525
|
+
# {.autologin_service} or {.link_or_create_with_auth_data}. Passing
|
|
526
|
+
# those keys directly bypasses the SDK's federated-identity wrapper
|
|
527
|
+
# and risks returning a victim's sessionToken to whoever submitted
|
|
528
|
+
# the request.
|
|
529
|
+
#
|
|
530
|
+
# @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
|
|
531
|
+
# @option opts [Boolean] :master_key Whether the master key should be used for this request.
|
|
532
|
+
# @raise [ArgumentError] If +body+ contains +authData+/+auth_data+/+objectId+ — use {.autologin_service} for federated flows.
|
|
533
|
+
# @raise [Parse::Error::UsernameMissingError] If username is missing.
|
|
534
|
+
# @raise [Parse::Error::PasswordMissingError] If password is missing.
|
|
535
|
+
# @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
|
|
536
|
+
# @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
|
|
537
|
+
# @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
|
|
538
|
+
# @raise [Parse::Client::ResponseError] An unknown error occurred.
|
|
539
|
+
# @return [User] Returns a successfully created Parse::User.
|
|
540
|
+
def self.create(body, **opts)
|
|
541
|
+
# Consume and clear the SDK-internal trust marker before validation
|
|
542
|
+
# or wire transit. This prevents trusted-authdata flag smuggling
|
|
543
|
+
# through callers that copy hashes from a request parameter.
|
|
544
|
+
trusted = body.is_a?(Hash) ? (body.delete(:__parse_stack_trusted_authdata) ||
|
|
545
|
+
body.delete("__parse_stack_trusted_authdata")) : false
|
|
546
|
+
assert_create_body_safe!(body) unless trusted
|
|
547
|
+
strip_server_controlled_keys!(body)
|
|
548
|
+
response = client.create_user(body, opts: opts)
|
|
549
|
+
if response.success?
|
|
550
|
+
body.delete :password # clear password before merging
|
|
551
|
+
return Parse::User.build body.merge(response.result)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
case response.code
|
|
555
|
+
when Parse::Response::ERROR_USERNAME_MISSING
|
|
556
|
+
raise Parse::Error::UsernameMissingError, response
|
|
557
|
+
when Parse::Response::ERROR_PASSWORD_MISSING
|
|
558
|
+
raise Parse::Error::PasswordMissingError, response
|
|
559
|
+
when Parse::Response::ERROR_USERNAME_TAKEN
|
|
560
|
+
raise Parse::Error::UsernameTakenError, response
|
|
561
|
+
when Parse::Response::ERROR_EMAIL_TAKEN
|
|
562
|
+
raise Parse::Error::EmailTakenError, response
|
|
563
|
+
end
|
|
564
|
+
raise Parse::Client::ResponseError, response
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# @!visibility private
|
|
568
|
+
# Silently strips {SERVER_CONTROLLED_KEYS} from +body+ in place. Used
|
|
569
|
+
# by {.create}, {#signup!}, and {#signup_create} as defense-in-depth so
|
|
570
|
+
# caller-supplied values for fields that Parse Server is meant to
|
|
571
|
+
# control (currently just +emailVerified+) never reach the wire.
|
|
572
|
+
# @return [Hash, Object] the same +body+ object, mutated.
|
|
573
|
+
def self.strip_server_controlled_keys!(body)
|
|
574
|
+
return body unless body.is_a?(Hash)
|
|
575
|
+
SERVER_CONTROLLED_KEYS.each do |k|
|
|
576
|
+
body.delete(k)
|
|
577
|
+
body.delete(k.to_s)
|
|
578
|
+
end
|
|
579
|
+
body
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# @!visibility private
|
|
583
|
+
# Raises +ArgumentError+ if +body+ carries keys that would let an
|
|
584
|
+
# attacker turn +Parse::User.create+ into an account-takeover sink.
|
|
585
|
+
# Skipped when called through the SDK's federated-identity wrapper
|
|
586
|
+
# ({.autologin_service}), which deliberately supplies +authData+ and
|
|
587
|
+
# is responsible for its provenance.
|
|
588
|
+
def self.assert_create_body_safe!(body)
|
|
589
|
+
return unless body.is_a?(Hash)
|
|
590
|
+
unsafe = body.each_key.select do |k|
|
|
591
|
+
ks = k.is_a?(String) ? k.to_sym : k
|
|
592
|
+
UNSAFE_CREATE_KEYS.include?(ks)
|
|
593
|
+
end
|
|
594
|
+
unless unsafe.empty?
|
|
595
|
+
raise ArgumentError,
|
|
596
|
+
"Refusing Parse::User.create with #{unsafe.inspect}. " \
|
|
597
|
+
"These keys can be used for account takeover via federated-id " \
|
|
598
|
+
"linking. Use Parse::User.autologin_service for federated " \
|
|
599
|
+
"flows, or pass authData via that wrapper."
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Automatically and implicitly signup a user if it did not already exists and
|
|
604
|
+
# authenticates them (login) using third-party authentication data. May raise exceptions
|
|
605
|
+
# similar to `create` depending on what you provide the _body_ parameter.
|
|
606
|
+
# @param service_name [Symbol] the name of the service key (ex. :facebook)
|
|
607
|
+
# @param auth_data [Hash] the specific service data to place in the user's auth-data for this service.
|
|
608
|
+
# @param body [Hash] any additional User related fields or properties when signing up this User record.
|
|
609
|
+
# @return [User] a logged in user, or nil.
|
|
610
|
+
# @see User.create
|
|
611
|
+
def self.autologin_service(service_name, auth_data, body: {})
|
|
612
|
+
# Trust-mark this call so {.assert_create_body_safe!} permits the
|
|
613
|
+
# +authData+ that we are explicitly responsible for here. The
|
|
614
|
+
# marker is consumed inside {.create} before forwarding to the
|
|
615
|
+
# server.
|
|
616
|
+
body = body.merge({
|
|
617
|
+
authData: { service_name => auth_data },
|
|
618
|
+
__parse_stack_trusted_authdata: true,
|
|
619
|
+
})
|
|
620
|
+
self.create(body)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# This method will signup a new user using the parameters below. The required fields
|
|
624
|
+
# to create a user in Parse is the _username_ and _password_ fields. The _email_ field is optional.
|
|
625
|
+
# Both _username_ and _email_ (if provided), must be unique. At a minimum, it is recommended you perform
|
|
626
|
+
# a query using the supplied _username_ first to verify do not already have an account with that username.
|
|
627
|
+
# This method will raise all the exceptions from the similar `create` method.
|
|
628
|
+
# @see User.create
|
|
629
|
+
def self.signup(username, password, email = nil, body: {})
|
|
630
|
+
body = body.merge({ username: username, password: password })
|
|
631
|
+
body[:email] = email if email.present?
|
|
632
|
+
self.create(body)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Login and return a Parse::User with this username/password combination.
|
|
636
|
+
# @param username [String] the user's username
|
|
637
|
+
# @param password [String] the user's password
|
|
638
|
+
# @return [User] a logged in user for the provided username. Returns nil otherwise.
|
|
639
|
+
def self.login(username, password)
|
|
640
|
+
response = client.login(username.to_s, password.to_s)
|
|
641
|
+
response.success? ? Parse::User.build(response.result) : nil
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Request a password reset for a registered email.
|
|
645
|
+
# @example
|
|
646
|
+
# user = Parse::User.first
|
|
647
|
+
#
|
|
648
|
+
# # pass a user object
|
|
649
|
+
# Parse::User.request_password_reset user
|
|
650
|
+
# # or email
|
|
651
|
+
# Parse::User.request_password_reset("user@example.com")
|
|
652
|
+
# @param email [String] The user's email address.
|
|
653
|
+
# @return [Boolean] True/false if successful.
|
|
654
|
+
def self.request_password_reset(email)
|
|
655
|
+
email = email.email if email.is_a?(Parse::User)
|
|
656
|
+
return false if email.blank?
|
|
657
|
+
response = client.request_password_reset(email)
|
|
658
|
+
response.success?
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Same as `session!` but returns nil if a user was not found or sesion token was invalid.
|
|
662
|
+
# @return [User] the user matching this active token, otherwise nil.
|
|
663
|
+
# @see #session!
|
|
664
|
+
def self.session(token, opts = {})
|
|
665
|
+
self.session! token, opts
|
|
666
|
+
rescue Parse::Error::InvalidSessionTokenError
|
|
667
|
+
nil
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Return a Parse::User for this active session token.
|
|
671
|
+
# @raise [InvalidSessionTokenError] Invalid session token.
|
|
672
|
+
# @return [User] the user matching this active token
|
|
673
|
+
# @see #session
|
|
674
|
+
def self.session!(token, opts = {})
|
|
675
|
+
# support Parse::Session objects
|
|
676
|
+
token = token.session_token if token.respond_to?(:session_token)
|
|
677
|
+
response = client.current_user(token, **opts)
|
|
678
|
+
response.success? ? Parse::User.build(response.result) : nil
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# If the current session token for this instance is nil, this method finds
|
|
682
|
+
# the most recent active Parse::Session token for this user and applies it to the instance.
|
|
683
|
+
# The user instance will now be authenticated and logged in with the selected session token.
|
|
684
|
+
# Useful if you need to call save or destroy methods on behalf of a logged in user.
|
|
685
|
+
# @return [String] The session token or nil if no session was found for this user.
|
|
686
|
+
def any_session!
|
|
687
|
+
unless @session_token.present?
|
|
688
|
+
_active_session = active_sessions(restricted: false, order: :updated_at.desc).first
|
|
689
|
+
self.session_token = _active_session.session_token if _active_session.present?
|
|
690
|
+
end
|
|
691
|
+
@session_token
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# =========================================================================
|
|
695
|
+
# Session Management Methods
|
|
696
|
+
# =========================================================================
|
|
697
|
+
|
|
698
|
+
# Logout from all sessions, effectively signing out on all devices.
|
|
699
|
+
# Optionally keep the current session active.
|
|
700
|
+
# @param keep_current [Boolean] if true, keeps the current session active (default: false)
|
|
701
|
+
# @return [Integer] the number of sessions revoked
|
|
702
|
+
# @example
|
|
703
|
+
# # Logout from all devices
|
|
704
|
+
# user.logout_all!
|
|
705
|
+
#
|
|
706
|
+
# # Logout from all devices except current
|
|
707
|
+
# user.logout_all!(keep_current: true)
|
|
708
|
+
def logout_all!(keep_current: false)
|
|
709
|
+
return 0 unless id.present?
|
|
710
|
+
except_token = keep_current ? @session_token : nil
|
|
711
|
+
count = Parse::Session.revoke_all_for_user(self, except: except_token)
|
|
712
|
+
@session_token = nil unless keep_current
|
|
713
|
+
@session = nil unless keep_current
|
|
714
|
+
count
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Get the count of active (non-expired) sessions for this user.
|
|
718
|
+
# @return [Integer] the number of active sessions
|
|
719
|
+
# @example
|
|
720
|
+
# count = user.active_session_count
|
|
721
|
+
# puts "User is logged in on #{count} devices"
|
|
722
|
+
def active_session_count
|
|
723
|
+
return 0 unless id.present?
|
|
724
|
+
Parse::Session.active_count_for_user(self)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Get all active sessions for this user.
|
|
728
|
+
# @return [Array<Parse::Session>] array of active session objects
|
|
729
|
+
# @example
|
|
730
|
+
# user.sessions.each do |session|
|
|
731
|
+
# puts "Session created: #{session.created_at}"
|
|
732
|
+
# end
|
|
733
|
+
def sessions
|
|
734
|
+
return [] unless id.present?
|
|
735
|
+
Parse::Session.for_user(self).all
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Check if this user has multiple active sessions (logged in on multiple devices).
|
|
739
|
+
# @return [Boolean] true if user has more than one active session
|
|
740
|
+
# @example
|
|
741
|
+
# if user.multi_session?
|
|
742
|
+
# puts "User is logged in on multiple devices"
|
|
743
|
+
# end
|
|
744
|
+
def multi_session?
|
|
745
|
+
active_session_count > 1
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Return the transitive upward closure of role names this user
|
|
749
|
+
# inherits permissions from.
|
|
750
|
+
#
|
|
751
|
+
# ## Authorization
|
|
752
|
+
#
|
|
753
|
+
# The role graph is privileged data: Parse Server's `_Role` class
|
|
754
|
+
# ships with `acl_policy :private` precisely so anonymous clients
|
|
755
|
+
# cannot enumerate role memberships. This method therefore routes
|
|
756
|
+
# through the mongo-direct fast path under an EXPLICIT
|
|
757
|
+
# authorization scope.
|
|
758
|
+
#
|
|
759
|
+
# By default, `as:` is set to `self` — the user instance itself,
|
|
760
|
+
# meaning "I (this user) am asking about my own roles". The scope
|
|
761
|
+
# is resolved via {Parse::ACLScope} and CLP is enforced against
|
|
762
|
+
# `_Role`: the call succeeds iff the user's permission set
|
|
763
|
+
# (`["*", user.id, "role:..."]`) is permitted to `find` on
|
|
764
|
+
# `_Role`. Under Parse Server's default `_Role` CLP (master-only,
|
|
765
|
+
# which {Parse::Role}'s `acl_policy :private` does not change),
|
|
766
|
+
# the user's scope is NOT permitted, so this call raises
|
|
767
|
+
# {Parse::CLPScope::Denied}. Apps that have explicitly opened
|
|
768
|
+
# `_Role` CLP for authenticated users (e.g. `find:
|
|
769
|
+
# { requiresAuthentication: true }`) will have the call succeed.
|
|
770
|
+
#
|
|
771
|
+
# Callers performing privileged work (computing ACL permission
|
|
772
|
+
# sets, e.g. server-side filters) should pass `master: true` to
|
|
773
|
+
# bypass the CLP check.
|
|
774
|
+
#
|
|
775
|
+
# **Breaking change:** Previously this method bypassed the
|
|
776
|
+
# authorization check entirely (callers could construct a
|
|
777
|
+
# `Parse::User` with any objectId via
|
|
778
|
+
# `Parse::User.new.tap { |u| u.id = victim_id }` and enumerate
|
|
779
|
+
# the victim's roles). The new contract is explicit-auth-required;
|
|
780
|
+
# use `master: true` for the previous behavior.
|
|
781
|
+
#
|
|
782
|
+
# @param max_depth [Integer] maximum BFS depth (default: 10).
|
|
783
|
+
# @param master [Boolean] when +true+, bypass `_Role` CLP and run
|
|
784
|
+
# the role-graph lookup under master mode. Use for ACL-building
|
|
785
|
+
# code paths inside the SDK or in admin tooling.
|
|
786
|
+
# @param as [Parse::User, Parse::Pointer, nil] caller-scope. When
|
|
787
|
+
# `nil`, defaults to `self` (the user-asking-about-their-own-roles
|
|
788
|
+
# case). Pass a different user to ask "what would this caller
|
|
789
|
+
# see when introspecting this user's roles?"; the scope's
|
|
790
|
+
# permission set is checked against `_Role` CLP.
|
|
791
|
+
# @return [Set<String>] role names (no +role:+ prefix). Empty set
|
|
792
|
+
# when the user has no objectId yet or holds no roles.
|
|
793
|
+
# @raise [Parse::CLPScope::Denied] when the scope cannot `find`
|
|
794
|
+
# on `_Role` under the current CLP.
|
|
795
|
+
# @example
|
|
796
|
+
# # User reading their own roles (subject to _Role CLP):
|
|
797
|
+
# permission_set = (["*", user.id] + user.acl_roles.map { |n| "role:#{n}" }).uniq
|
|
798
|
+
# # Admin/SDK-internal code building ACL filters:
|
|
799
|
+
# permission_set = (["*", user.id] + user.acl_roles(master: true).map { |n| "role:#{n}" }).uniq
|
|
800
|
+
def acl_roles(max_depth: 10, master: false, as: nil)
|
|
801
|
+
return Set.new unless id.is_a?(String) && !id.empty?
|
|
802
|
+
# Default `as:` to self so the common "user reading their own
|
|
803
|
+
# roles" case works without ceremony when _Role CLP permits the
|
|
804
|
+
# user. The CLP check + scope resolution happens inside
|
|
805
|
+
# Parse::Role.all_for_user → Parse::MongoDB.role_names_for_user.
|
|
806
|
+
effective_as = as.nil? && master != true ? self : as
|
|
807
|
+
Parse::Role.all_for_user(
|
|
808
|
+
self, max_depth: max_depth, master: master, as: effective_as,
|
|
809
|
+
)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
private
|
|
813
|
+
|
|
814
|
+
# Keys that {#signup_create} will accept from a `POST /parse/users`
|
|
815
|
+
# response body and feed through {#set_attributes!}. `sessionToken`
|
|
816
|
+
# is the operative output of the signup endpoint; `emailVerified` is
|
|
817
|
+
# the only other field Parse Server commonly emits and is harmless to
|
|
818
|
+
# apply. All other keys are dropped, even if the server response
|
|
819
|
+
# contains them — this blocks a compromised or MITM'd Parse Server
|
|
820
|
+
# from planting `authData`, `_rperm`, `_wperm`, `roles`, or other
|
|
821
|
+
# security-sensitive fields into the in-memory user object via the
|
|
822
|
+
# save-as-signup path. `objectId`, `createdAt`, and `updatedAt` are
|
|
823
|
+
# extracted directly into the corresponding `@`-vars below and so do
|
|
824
|
+
# not need to appear in this list.
|
|
825
|
+
SIGNUP_RESPONSE_APPLY_KEYS = %w[sessionToken emailVerified].freeze
|
|
826
|
+
|
|
827
|
+
# Strict matcher for a client-supplied `objectId` that the SDK could
|
|
828
|
+
# plausibly have generated via Parse::Core::ParseReference. Used by
|
|
829
|
+
# {.signup_body_self_only_acl_safe?} to gate the narrow whitelist of
|
|
830
|
+
# client-supplied ACL+objectId pairs allowed through the signup body.
|
|
831
|
+
PARSE_OBJECT_ID_FORMAT = /\A[A-Za-z0-9]{10}\z/.freeze
|
|
832
|
+
|
|
833
|
+
# True when the signup-body `objectId` and `ACL` together describe the
|
|
834
|
+
# safe self-only ownership pattern that {acl_policy} produces under
|
|
835
|
+
# `owner: :self`: the body has a client-assigned `objectId` matching
|
|
836
|
+
# the Parse-id format, and the ACL has exactly one entry granting
|
|
837
|
+
# read+write to that same objectId. Any deviation — multiple keys, a
|
|
838
|
+
# non-self key, a `*` (public) entry, a `role:` entry, missing or
|
|
839
|
+
# extra permissions — fails the check and the strip-everything fallback
|
|
840
|
+
# in {#signup_create} / {#signup!} runs as before.
|
|
841
|
+
# @param body [Hash] signup request body, with symbol or string keys.
|
|
842
|
+
# @return [Boolean]
|
|
843
|
+
# @api private
|
|
844
|
+
def self.signup_body_self_only_acl_safe?(body)
|
|
845
|
+
return false unless body.is_a?(Hash)
|
|
846
|
+
oid = body[:objectId] || body["objectId"]
|
|
847
|
+
acl = body[:ACL] || body["ACL"]
|
|
848
|
+
return false unless oid.is_a?(String) && oid.match?(PARSE_OBJECT_ID_FORMAT)
|
|
849
|
+
return false unless acl.is_a?(Hash) && acl.size == 1
|
|
850
|
+
perms = acl[oid] || acl[oid.to_s]
|
|
851
|
+
return false unless perms.is_a?(Hash)
|
|
852
|
+
normalized = perms.transform_keys(&:to_s)
|
|
853
|
+
normalized == { "read" => true, "write" => true }
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Body of {#create} when signup-on-save applies. Mirrors the inherited
|
|
857
|
+
# Parse::Object create path but uses `create_user` (signup endpoint)
|
|
858
|
+
# instead of `create_object`, and so picks up the `sessionToken` that
|
|
859
|
+
# Parse Server only emits on the signup endpoint. Errors are not
|
|
860
|
+
# promoted to typed exceptions here (see {#signup!} for that variant);
|
|
861
|
+
# the response's success flag is returned so the caller's `save` /
|
|
862
|
+
# `save!` handles the failure via the standard `RecordNotSaved` path.
|
|
863
|
+
def signup_create
|
|
864
|
+
run_callbacks :create do
|
|
865
|
+
body = attribute_updates
|
|
866
|
+
# Strip server-managed and special fields from the request body.
|
|
867
|
+
# createdAt/updatedAt are always stripped (purely server-managed).
|
|
868
|
+
# objectId/ACL are normally stripped too (to prevent a caller
|
|
869
|
+
# planting a permissive ACL or a colliding objectId), but the
|
|
870
|
+
# narrow self-only ownership pattern produced by
|
|
871
|
+
# `acl_policy ..., owner: :self` is allowed through so the user
|
|
872
|
+
# can be created with self-R/W-only ACL in a single roundtrip.
|
|
873
|
+
if self.class.signup_body_self_only_acl_safe?(body)
|
|
874
|
+
body.except!(:createdAt, :updatedAt, "createdAt", "updatedAt")
|
|
875
|
+
else
|
|
876
|
+
body.except!(*Parse::Properties::BASE_FIELD_MAP.flatten)
|
|
877
|
+
end
|
|
878
|
+
self.class.strip_server_controlled_keys!(body)
|
|
879
|
+
# Anonymous signup: do NOT forward the caller's session token to
|
|
880
|
+
# POST /parse/users. The caller may be authenticated for an
|
|
881
|
+
# unrelated reason (e.g., an admin app session running a signup
|
|
882
|
+
# flow on behalf of someone else), but the user being created is
|
|
883
|
+
# by definition someone new. Forwarding `_session_token` makes
|
|
884
|
+
# Cloud Code `beforeSave(_User)` see `request.user = caller`,
|
|
885
|
+
# which an integrator can mistake for "the new user". The signup
|
|
886
|
+
# endpoint authenticates by the signup itself, not by a prior
|
|
887
|
+
# session — pass `nil` explicitly. Master key continues to flow
|
|
888
|
+
# via the normal authentication middleware when configured.
|
|
889
|
+
res = client.create_user(body, session_token: nil)
|
|
890
|
+
unless res.error?
|
|
891
|
+
result = res.result
|
|
892
|
+
@id = result[Parse::Model::OBJECT_ID] || @id
|
|
893
|
+
@created_at = result["createdAt"] || @created_at
|
|
894
|
+
@updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
|
|
895
|
+
# Plaintext password is no longer needed locally; the server
|
|
896
|
+
# has it hashed. Direct ivar assignment avoids re-dirtying the
|
|
897
|
+
# field.
|
|
898
|
+
@password = nil
|
|
899
|
+
set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
|
|
900
|
+
# Promote the freshly-applied session token into `@_session_token`
|
|
901
|
+
# so any in-flight after_create callback that calls back through
|
|
902
|
+
# the SDK authenticates as the just-signed-up user. Without this,
|
|
903
|
+
# the after_create `_assign_<field>!` callback installed by
|
|
904
|
+
# `parse_reference` (and any other after_create hook that issues
|
|
905
|
+
# an `update!`) reads `_session_token` (actions.rb:732) and finds
|
|
906
|
+
# nil — `client.update_object(..., session_token: nil)` then
|
|
907
|
+
# silently falls back to the master key under any configuration
|
|
908
|
+
# that supplies one (client.rb:682-687 only attaches the session
|
|
909
|
+
# token when `present?`; `DISABLE_MASTER_KEY` is not set on the
|
|
910
|
+
# nil branch). The result was a user-scoped PUT silently
|
|
911
|
+
# escalated to master-key authority, bypassing CLP and
|
|
912
|
+
# `request.user` checks in `beforeSave` cloud code. Promoting
|
|
913
|
+
# the new user's own session token here scopes the follow-up
|
|
914
|
+
# update to the just-created user — the appropriate authority
|
|
915
|
+
# for writes to their own row. The outer `save` zeroes
|
|
916
|
+
# `@_session_token` again at actions.rb:830, so the promotion
|
|
917
|
+
# is bounded by this in-flight save. The trust boundary here
|
|
918
|
+
# is identical to the existing `SIGNUP_RESPONSE_APPLY_KEYS`
|
|
919
|
+
# contract: the SDK already trusts `sessionToken` from a signup
|
|
920
|
+
# response (it has to, to honor the signup contract); this fix
|
|
921
|
+
# routes that same token to the in-flight auth context.
|
|
922
|
+
@_session_token = @session_token if @session_token.present?
|
|
923
|
+
# Clear dirty state BEFORE the `after_create` callback chain
|
|
924
|
+
# fires. If a subclass declares `parse_reference` (default
|
|
925
|
+
# field name with `precompute: false`), the after_create
|
|
926
|
+
# `_assign_<field>!` callback issues an `update!` from inside
|
|
927
|
+
# this `run_callbacks :create` block — and `attribute_updates`
|
|
928
|
+
# would otherwise still carry `password` as dirty with a nil
|
|
929
|
+
# current value, serializing as `password: { __op: "Delete" }`.
|
|
930
|
+
# Parse Server's `_User` write path feeds that hash to
|
|
931
|
+
# `@node-rs/bcrypt`, which raises
|
|
932
|
+
# `Value is non of these types TypedArray<u8>, String`. Same
|
|
933
|
+
# cleanup as `signup!`, just timed so the after_create
|
|
934
|
+
# callbacks see a clean dirty set.
|
|
935
|
+
changes_applied!
|
|
936
|
+
clear_partial_fetch_state!
|
|
937
|
+
end
|
|
938
|
+
puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
|
|
939
|
+
res.success?
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
end
|