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,310 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
# Multi-Factor Authentication (MFA) support for Parse Server.
|
|
6
|
+
#
|
|
7
|
+
# This module interfaces with Parse Server's built-in MFA adapter which supports
|
|
8
|
+
# TOTP (Time-based One-Time Password) and SMS-based authentication.
|
|
9
|
+
#
|
|
10
|
+
# == Parse Server Configuration
|
|
11
|
+
#
|
|
12
|
+
# MFA must be enabled in your Parse Server configuration:
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# auth: {
|
|
16
|
+
# mfa: {
|
|
17
|
+
# enabled: true,
|
|
18
|
+
# options: ["TOTP"], // or ["SMS", "TOTP"]
|
|
19
|
+
# digits: 6,
|
|
20
|
+
# period: 30,
|
|
21
|
+
# algorithm: "SHA1"
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
# == TOTP Setup Flow
|
|
27
|
+
#
|
|
28
|
+
# 1. Generate a secret client-side using {MFA.generate_secret}
|
|
29
|
+
# 2. Display QR code to user using {MFA.provisioning_uri} or {MFA.qr_code}
|
|
30
|
+
# 3. User scans QR with authenticator app (Google Authenticator, Authy, etc.)
|
|
31
|
+
# 4. User enters the 6-digit code from their app
|
|
32
|
+
# 5. Call {User#setup_mfa!} with secret and token to enable MFA
|
|
33
|
+
# 6. Store the recovery codes returned - user needs these for account recovery!
|
|
34
|
+
#
|
|
35
|
+
# @example Enable TOTP MFA for a user
|
|
36
|
+
# # Step 1: Generate secret
|
|
37
|
+
# secret = Parse::MFA.generate_secret
|
|
38
|
+
#
|
|
39
|
+
# # Step 2: Show QR code to user
|
|
40
|
+
# qr_svg = Parse::MFA.qr_code(secret, user.email, issuer: "MyApp")
|
|
41
|
+
# # render qr_svg in your UI
|
|
42
|
+
#
|
|
43
|
+
# # Step 3-4: User scans and enters code
|
|
44
|
+
# token = params[:totp_code] # "123456" from authenticator app
|
|
45
|
+
#
|
|
46
|
+
# # Step 5: Enable MFA
|
|
47
|
+
# recovery_codes = user.setup_mfa!(secret: secret, token: token)
|
|
48
|
+
# # => "ABC123DEF456..., XYZ789..."
|
|
49
|
+
#
|
|
50
|
+
# # Step 6: Show recovery codes to user (one time only!)
|
|
51
|
+
#
|
|
52
|
+
# @example Login with MFA
|
|
53
|
+
# user = Parse::User.login_with_mfa("username", "password", "123456")
|
|
54
|
+
#
|
|
55
|
+
# @see https://github.com/parse-community/parse-server/blob/master/src/Adapters/Auth/mfa.js
|
|
56
|
+
#
|
|
57
|
+
module MFA
|
|
58
|
+
# Error raised when MFA verification fails
|
|
59
|
+
class VerificationError < Parse::Error
|
|
60
|
+
def initialize(message = "Invalid MFA token")
|
|
61
|
+
super(message)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Error raised when MFA is required but not provided
|
|
66
|
+
class RequiredError < Parse::Error
|
|
67
|
+
def initialize(message = "MFA token is required for this account")
|
|
68
|
+
super(message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Error raised when MFA is already set up
|
|
73
|
+
class AlreadyEnabledError < Parse::Error
|
|
74
|
+
def initialize(message = "MFA is already set up on this account")
|
|
75
|
+
super(message)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Error raised when required gem is not available
|
|
80
|
+
class DependencyError < Parse::Error
|
|
81
|
+
def initialize(gem_name)
|
|
82
|
+
super("The '#{gem_name}' gem is required for this feature. Add to Gemfile: gem '#{gem_name}'")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Error raised when an operator is not authorized to perform a
|
|
87
|
+
# privileged MFA operation (e.g. master-key disable). See
|
|
88
|
+
# {Parse::MFA::UserExtension#disable_mfa_master_key!}.
|
|
89
|
+
class ForbiddenError < Parse::Error
|
|
90
|
+
def initialize(message = "Not authorized to perform this MFA operation")
|
|
91
|
+
super(message)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Default configuration
|
|
96
|
+
DEFAULT_CONFIG = {
|
|
97
|
+
issuer: "Parse App",
|
|
98
|
+
digits: 6,
|
|
99
|
+
period: 30,
|
|
100
|
+
algorithm: "SHA1",
|
|
101
|
+
secret_length: 20, # Minimum required by Parse Server
|
|
102
|
+
}.freeze
|
|
103
|
+
|
|
104
|
+
class << self
|
|
105
|
+
# Global MFA configuration
|
|
106
|
+
# @return [Hash]
|
|
107
|
+
def config
|
|
108
|
+
@config ||= DEFAULT_CONFIG.dup
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Configure MFA settings
|
|
112
|
+
# @yield [config] Configuration hash
|
|
113
|
+
# @example
|
|
114
|
+
# Parse::MFA.configure do |config|
|
|
115
|
+
# config[:issuer] = "My App"
|
|
116
|
+
# end
|
|
117
|
+
def configure
|
|
118
|
+
yield config if block_given?
|
|
119
|
+
config
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if rotp gem is available
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
def rotp_available?
|
|
125
|
+
require "rotp"
|
|
126
|
+
true
|
|
127
|
+
rescue LoadError
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Check if rqrcode gem is available
|
|
132
|
+
# @return [Boolean]
|
|
133
|
+
def rqrcode_available?
|
|
134
|
+
require "rqrcode"
|
|
135
|
+
true
|
|
136
|
+
rescue LoadError
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Generate a new TOTP secret for MFA setup.
|
|
141
|
+
# The secret must be at least 20 characters (Parse Server requirement).
|
|
142
|
+
#
|
|
143
|
+
# @param length [Integer] Secret length (minimum 20)
|
|
144
|
+
# @return [String] Base32-encoded secret
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# secret = Parse::MFA.generate_secret
|
|
148
|
+
# # => "JBSWY3DPEHPK3PXP4QFAZJ7K"
|
|
149
|
+
def generate_secret(length: nil)
|
|
150
|
+
ensure_rotp!
|
|
151
|
+
length ||= config[:secret_length]
|
|
152
|
+
length = [length, 20].max # Parse Server requires minimum 20
|
|
153
|
+
ROTP::Base32.random(length)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Create a TOTP instance for verification.
|
|
157
|
+
#
|
|
158
|
+
# @param secret [String] Base32-encoded secret
|
|
159
|
+
# @param issuer [String] Optional issuer name
|
|
160
|
+
# @return [ROTP::TOTP]
|
|
161
|
+
def totp(secret, issuer: nil)
|
|
162
|
+
ensure_rotp!
|
|
163
|
+
ROTP::TOTP.new(
|
|
164
|
+
secret,
|
|
165
|
+
issuer: issuer || config[:issuer],
|
|
166
|
+
interval: config[:period],
|
|
167
|
+
digits: config[:digits],
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Verify a TOTP code locally (for testing/validation before sending to server).
|
|
172
|
+
#
|
|
173
|
+
# @param secret [String] Base32-encoded secret
|
|
174
|
+
# @param code [String] The 6-digit code to verify
|
|
175
|
+
# @return [Boolean] True if valid
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# if Parse::MFA.verify(secret, "123456")
|
|
179
|
+
# puts "Code is valid!"
|
|
180
|
+
# end
|
|
181
|
+
def verify(secret, code)
|
|
182
|
+
return false if secret.blank? || code.blank?
|
|
183
|
+
|
|
184
|
+
ensure_rotp!
|
|
185
|
+
drift_seconds = config[:period]
|
|
186
|
+
totp_instance = totp(secret)
|
|
187
|
+
totp_instance.verify(code.to_s, drift_behind: drift_seconds, drift_ahead: drift_seconds).present?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get the current TOTP code (for testing/debugging).
|
|
191
|
+
#
|
|
192
|
+
# @param secret [String] Base32-encoded secret
|
|
193
|
+
# @return [String] Current 6-digit code
|
|
194
|
+
def current_code(secret)
|
|
195
|
+
ensure_rotp!
|
|
196
|
+
totp(secret).now
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Generate provisioning URI for authenticator apps.
|
|
200
|
+
#
|
|
201
|
+
# @param secret [String] Base32-encoded secret
|
|
202
|
+
# @param account_name [String] User identifier (email or username)
|
|
203
|
+
# @param issuer [String] Optional issuer override
|
|
204
|
+
# @return [String] otpauth:// URI
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# uri = Parse::MFA.provisioning_uri(secret, "user@example.com", issuer: "MyApp")
|
|
208
|
+
# # => "otpauth://totp/MyApp:user@example.com?secret=ABC123&issuer=MyApp"
|
|
209
|
+
def provisioning_uri(secret, account_name, issuer: nil)
|
|
210
|
+
ensure_rotp!
|
|
211
|
+
totp(secret, issuer: issuer).provisioning_uri(account_name)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generate a QR code for the authenticator app.
|
|
215
|
+
#
|
|
216
|
+
# @param secret [String] Base32-encoded secret
|
|
217
|
+
# @param account_name [String] User identifier
|
|
218
|
+
# @param issuer [String] Optional issuer name
|
|
219
|
+
# @param format [Symbol] Output format (:svg, :png, :ascii)
|
|
220
|
+
# @return [String] QR code in specified format
|
|
221
|
+
#
|
|
222
|
+
# @example
|
|
223
|
+
# svg = Parse::MFA.qr_code(secret, user.email, issuer: "MyApp")
|
|
224
|
+
# # Render in HTML: <%= raw svg %>
|
|
225
|
+
def qr_code(secret, account_name, issuer: nil, format: :svg)
|
|
226
|
+
ensure_rqrcode!
|
|
227
|
+
uri = provisioning_uri(secret, account_name, issuer: issuer)
|
|
228
|
+
qr = RQRCode::QRCode.new(uri)
|
|
229
|
+
|
|
230
|
+
case format
|
|
231
|
+
when :svg
|
|
232
|
+
qr.as_svg(
|
|
233
|
+
color: "000",
|
|
234
|
+
shape_rendering: "crispEdges",
|
|
235
|
+
module_size: 4,
|
|
236
|
+
standalone: true,
|
|
237
|
+
)
|
|
238
|
+
when :png
|
|
239
|
+
qr.as_png(size: 300)
|
|
240
|
+
when :ascii
|
|
241
|
+
qr.as_ansi
|
|
242
|
+
else
|
|
243
|
+
qr.as_svg
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Build authData hash for MFA setup.
|
|
248
|
+
#
|
|
249
|
+
# @param secret [String] Base32-encoded TOTP secret
|
|
250
|
+
# @param token [String] Current TOTP code for verification
|
|
251
|
+
# @return [Hash] authData for Parse Server
|
|
252
|
+
def build_setup_auth_data(secret:, token:)
|
|
253
|
+
{
|
|
254
|
+
mfa: {
|
|
255
|
+
secret: secret,
|
|
256
|
+
token: token,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Build authData hash for MFA login.
|
|
262
|
+
#
|
|
263
|
+
# @param token [String] TOTP code or recovery code
|
|
264
|
+
# @return [Hash] authData for Parse Server
|
|
265
|
+
def build_login_auth_data(token:)
|
|
266
|
+
{
|
|
267
|
+
mfa: {
|
|
268
|
+
token: token,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Build authData hash for SMS MFA setup.
|
|
274
|
+
#
|
|
275
|
+
# @param mobile [String] Phone number in E.164 format
|
|
276
|
+
# @return [Hash] authData for Parse Server
|
|
277
|
+
def build_sms_setup_auth_data(mobile:)
|
|
278
|
+
{
|
|
279
|
+
mfa: {
|
|
280
|
+
mobile: mobile,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Build authData hash for SMS MFA confirmation.
|
|
286
|
+
#
|
|
287
|
+
# @param mobile [String] Phone number
|
|
288
|
+
# @param token [String] SMS code received
|
|
289
|
+
# @return [Hash] authData for Parse Server
|
|
290
|
+
def build_sms_confirm_auth_data(mobile:, token:)
|
|
291
|
+
{
|
|
292
|
+
mfa: {
|
|
293
|
+
mobile: mobile,
|
|
294
|
+
token: token,
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private
|
|
300
|
+
|
|
301
|
+
def ensure_rotp!
|
|
302
|
+
raise DependencyError.new("rotp") unless rotp_available?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def ensure_rqrcode!
|
|
306
|
+
raise DependencyError.new("rqrcode") unless rqrcode_available?
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext/object"
|
|
8
|
+
require "active_support/core_ext/string"
|
|
9
|
+
require "active_support/core_ext"
|
|
10
|
+
require "active_model/serializers/json"
|
|
11
|
+
|
|
12
|
+
module Parse
|
|
13
|
+
class Webhooks
|
|
14
|
+
# Represents the data structure that Parse server sends to a registered webhook.
|
|
15
|
+
# Parse Parse allows you to receive Cloud Code webhooks on your own hosted
|
|
16
|
+
# server. The `Parse::Webhooks` class is a lightweight Rack application that
|
|
17
|
+
# routes incoming Cloud Code webhook requests and payloads to locally
|
|
18
|
+
# registered handlers. The payloads are {Parse::Webhooks::Payload} type of objects that
|
|
19
|
+
# represent that data that Parse sends webhook handlers.
|
|
20
|
+
class Payload
|
|
21
|
+
# The set of keys that can be contained in a Parse hash payload for a webhook.
|
|
22
|
+
ATTRIBUTES = { master: nil, user: nil,
|
|
23
|
+
installationId: nil, params: nil,
|
|
24
|
+
functionName: nil, object: nil,
|
|
25
|
+
original: nil, update: nil,
|
|
26
|
+
query: nil, log: nil,
|
|
27
|
+
objects: nil,
|
|
28
|
+
triggerName: nil }.freeze
|
|
29
|
+
include ::ActiveModel::Serializers::JSON
|
|
30
|
+
# @!attribute [rw] master
|
|
31
|
+
# @return [Boolean] whether the master key was used for this request.
|
|
32
|
+
# @!attribute [rw] user
|
|
33
|
+
# @return [Parse::User] the user who performed this request or action.
|
|
34
|
+
# @!attribute [rw] installation_id
|
|
35
|
+
# @return [String] The identifier of the device that submitted the request.
|
|
36
|
+
# @!attribute [rw] params
|
|
37
|
+
# @return [Hash] The list of function arguments submitted for a function request.
|
|
38
|
+
# @!attribute [rw] function_name
|
|
39
|
+
# @return [String] the name of the function.
|
|
40
|
+
# @!attribute [rw] object
|
|
41
|
+
# In a beforeSave, this attribute is the final object that will be persisted.
|
|
42
|
+
# @return [Hash] the object hash related to a webhook trigger request.
|
|
43
|
+
# @see #parse_object
|
|
44
|
+
# @!attribute [rw] trigger_name
|
|
45
|
+
# @return [String] the name of the trigger (ex. beforeSave, afterSave, etc.)
|
|
46
|
+
# @!attribute [rw] original
|
|
47
|
+
# In a beforeSave, for previously saved objects, this attribute is the Parse::Object
|
|
48
|
+
# that was previously in the persistent store.
|
|
49
|
+
# @return [Hash] the object hash related to a webhook trigger request.
|
|
50
|
+
# @see #parse_object
|
|
51
|
+
# @!attribute [rw] raw
|
|
52
|
+
# @return [Hash] the raw payload from Parse server.
|
|
53
|
+
# @!attribute [rw] update
|
|
54
|
+
# @return [Hash] the update payload in the request.
|
|
55
|
+
# @!attribute [r] query
|
|
56
|
+
# The query request in a beforeFind trigger. Available in Parse Server 2.3.1 or later.
|
|
57
|
+
# @return [Parse::Query]
|
|
58
|
+
# @!attribute [r] objects
|
|
59
|
+
# The set of matching objects in an afterFind trigger. Available in Parse Server 2.3.1 or later.
|
|
60
|
+
# @return [<Parse::Object>]
|
|
61
|
+
# @!attribute [r] log
|
|
62
|
+
# Logging information if available. Available in Parse Server 2.3.1 or later.
|
|
63
|
+
# @return [Hash] the set of matching objects in an afterFind trigger.
|
|
64
|
+
attr_accessor :master, :user, :installation_id, :params, :function_name, :object, :trigger_name
|
|
65
|
+
attr_accessor :query, :log, :objects
|
|
66
|
+
attr_accessor :original, :update, :raw
|
|
67
|
+
# @!visibility private
|
|
68
|
+
attr_accessor :webhook_class
|
|
69
|
+
alias_method :installationId, :installation_id
|
|
70
|
+
alias_method :functionName, :function_name
|
|
71
|
+
alias_method :triggerName, :trigger_name
|
|
72
|
+
|
|
73
|
+
# You would normally never create a {Parse::Webhooks::Payload} object since it is automatically
|
|
74
|
+
# provided to you when using Parse::Webhooks.
|
|
75
|
+
# @see Parse::Webhooks
|
|
76
|
+
def initialize(hash = {})
|
|
77
|
+
hash = JSON.parse(hash, max_nesting: 20) if hash.is_a?(String)
|
|
78
|
+
hash = Hash[hash.map { |k, v| [k.to_s.underscore.to_sym, v] }]
|
|
79
|
+
@raw = hash
|
|
80
|
+
@master = hash[:master]
|
|
81
|
+
# Strip protected mass-assignment keys (sessionToken, _rperm, _wperm,
|
|
82
|
+
# _hashed_password, authData, roles, etc.) BEFORE constructing the
|
|
83
|
+
# user object. Without this, an attacker reaching the webhook
|
|
84
|
+
# endpoint with a valid key (or with the optional unauthenticated
|
|
85
|
+
# mode enabled) can forge any of these fields on +payload.user+
|
|
86
|
+
# via the +objectId+-present hydration branch that bypasses the
|
|
87
|
+
# +Parse::Object#apply_attributes!+ protected-key filter.
|
|
88
|
+
if hash[:user].present?
|
|
89
|
+
@user = Parse::User.new(self.class.scrub_protected_keys(hash[:user]))
|
|
90
|
+
end
|
|
91
|
+
@installation_id = hash[:installation_id]
|
|
92
|
+
@params = hash[:params]
|
|
93
|
+
@params = @params.with_indifferent_access if @params.is_a?(Hash)
|
|
94
|
+
@function_name = hash[:function_name]
|
|
95
|
+
@object = self.class.scrub_protected_keys(hash[:object])
|
|
96
|
+
@trigger_name = hash[:trigger_name]
|
|
97
|
+
@original = self.class.scrub_protected_keys(hash[:original])
|
|
98
|
+
@update = self.class.scrub_protected_keys(hash[:update]) || {}
|
|
99
|
+
# Added for beforeFind and afterFind triggers
|
|
100
|
+
@query = hash[:query]
|
|
101
|
+
@objects = hash[:objects] || []
|
|
102
|
+
@log = hash[:log]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @!visibility private
|
|
106
|
+
# Routing metadata that must be preserved on payload hashes even
|
|
107
|
+
# though the general mass-assignment denylist forbids it. Stripping
|
|
108
|
+
# +className+ here breaks +parse_class+/+parse_object+ resolution and
|
|
109
|
+
# silently disables +payload_class_mismatch?+. The denylist still
|
|
110
|
+
# protects +Parse::Object#apply_attributes!+ at hydration time.
|
|
111
|
+
PAYLOAD_PRESERVED_KEYS = %w[className __type].freeze
|
|
112
|
+
|
|
113
|
+
# @!visibility private
|
|
114
|
+
# Returns a copy of +obj+ with the +PROTECTED_MASS_ASSIGNMENT_KEYS+
|
|
115
|
+
# removed, except for routing metadata in +PAYLOAD_PRESERVED_KEYS+.
|
|
116
|
+
# Operates on string and symbol keys (Parse Server uses camelCase
|
|
117
|
+
# strings on the wire; downstream code may have already symbolized).
|
|
118
|
+
# Pass-through for non-Hash input.
|
|
119
|
+
def self.scrub_protected_keys(obj)
|
|
120
|
+
return obj unless obj.is_a?(Hash)
|
|
121
|
+
denied = Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS
|
|
122
|
+
obj.reject do |k, _|
|
|
123
|
+
name = k.to_s
|
|
124
|
+
next false if PAYLOAD_PRESERVED_KEYS.include?(name)
|
|
125
|
+
denied.include?(name) || denied.include?(name.underscore)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [ATTRIBUTES]
|
|
130
|
+
def attributes
|
|
131
|
+
ATTRIBUTES
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Method to print to standard that utilizes the an internal id to make it easier
|
|
135
|
+
# to trace incoming requests.
|
|
136
|
+
def wlog(s)
|
|
137
|
+
# generates a unique random number in order to be used in logging. This
|
|
138
|
+
# is useful when debugging issues in production where one server instance
|
|
139
|
+
# may be running multiple threads and you want to trace the incoming call.
|
|
140
|
+
@rid ||= rand(999).to_s.rjust(3)
|
|
141
|
+
puts "[> #{@rid}] #{s}"
|
|
142
|
+
@rid
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# true if this is a webhook function request.
|
|
146
|
+
def function?
|
|
147
|
+
@function_name.present?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# true if the master key was used for this request.
|
|
151
|
+
def master?
|
|
152
|
+
@master.present?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [String] the name of the Parse class for this request.
|
|
156
|
+
def parse_class
|
|
157
|
+
return @webhook_class if @webhook_class.present?
|
|
158
|
+
return nil unless @object.present?
|
|
159
|
+
@object[Parse::Model::KEY_CLASS_NAME] || @object[:className]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [String] the objectId in this request.
|
|
163
|
+
def parse_id
|
|
164
|
+
return nil unless @object.present?
|
|
165
|
+
@object[Parse::Model::OBJECT_ID] || @object[:objectId]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
alias_method :objectId, :parse_id
|
|
169
|
+
|
|
170
|
+
# true if this is a webhook trigger request.
|
|
171
|
+
def trigger?
|
|
172
|
+
@trigger_name.present?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# true if this is a beforeSave or beforeDelete webhook trigger request.
|
|
176
|
+
def before_trigger?
|
|
177
|
+
before_save? || before_delete? || before_find?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# true if this is a afterSave or afterDelete webhook trigger request.
|
|
181
|
+
def after_trigger?
|
|
182
|
+
after_save? || after_delete? || after_find?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# true if this is a beforeSave webhook trigger request.
|
|
186
|
+
def before_save?
|
|
187
|
+
trigger? && @trigger_name.to_sym == :beforeSave
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# true if this is a afterSave webhook trigger request.
|
|
191
|
+
def after_save?
|
|
192
|
+
trigger? && @trigger_name.to_sym == :afterSave
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# true if this is a beforeDelete webhook trigger request.
|
|
196
|
+
def before_delete?
|
|
197
|
+
trigger? && @trigger_name.to_sym == :beforeDelete
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# true if this is a afterDelete webhook trigger request.
|
|
201
|
+
def after_delete?
|
|
202
|
+
trigger? && @trigger_name.to_sym == :afterDelete
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# true if this is a beforeFind webhook trigger request.
|
|
206
|
+
def before_find?
|
|
207
|
+
trigger? && @trigger_name.to_sym == :beforeFind
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# true if this is a afterFind webhook trigger request.
|
|
211
|
+
def after_find?
|
|
212
|
+
trigger? && @trigger_name.to_sym == :afterFind
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# true if this request is a trigger that contains an object.
|
|
216
|
+
def object?
|
|
217
|
+
trigger? && @object.present?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @return [Parse::Object] a Parse::Object from the original object
|
|
221
|
+
def original_parse_object
|
|
222
|
+
return nil unless @original.is_a?(Hash)
|
|
223
|
+
# Always pass the trigger's expected class explicitly so the
|
|
224
|
+
# className inside the payload cannot redirect this hydration to a
|
|
225
|
+
# different class.
|
|
226
|
+
Parse::Object.build(@original, parse_class)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# This method returns a Parse::Object by combining the original object, if was provided,
|
|
230
|
+
# with the final object. This will return a dirty tracked Parse::Object subclass,
|
|
231
|
+
# that will have information on which fields have changed between the previous state
|
|
232
|
+
# in the persistent store and the one about to be saved.
|
|
233
|
+
# @param pristine [Boolean] whether the object should be returned without dirty tracking.
|
|
234
|
+
# @return [Parse::Object] a dirty tracked Parse::Object subclass instance
|
|
235
|
+
def parse_object(pristine = false)
|
|
236
|
+
return nil unless object?
|
|
237
|
+
return Parse::Object.build(@object, parse_class) if pristine
|
|
238
|
+
# Memoize so pre-block guard application and the user webhook handler
|
|
239
|
+
# observe the same instance. Otherwise field_guards applied on the
|
|
240
|
+
# framework's pre-built object would be invisible to the block's
|
|
241
|
+
# later parse_object call (which would construct a fresh dirty-tracked
|
|
242
|
+
# object from @object/@original).
|
|
243
|
+
return @parse_object if defined?(@parse_object) && !@parse_object.nil?
|
|
244
|
+
@parse_object = build_parse_object
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# @!visibility private
|
|
248
|
+
# Returns +true+ when +@object+/+@original+ contain a className that
|
|
249
|
+
# disagrees with the trigger's expected class. Used to skip building
|
|
250
|
+
# a typed object when the payload was clearly forged or routed
|
|
251
|
+
# incorrectly.
|
|
252
|
+
def payload_class_mismatch?
|
|
253
|
+
expected = parse_class
|
|
254
|
+
return false if expected.nil?
|
|
255
|
+
[@object, @original, @update].any? do |h|
|
|
256
|
+
h.is_a?(Hash) && h["className"] && h["className"] != expected
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Force a fresh build, discarding any memoized parse_object. Used by the
|
|
261
|
+
# webhook framework after mutating @object / @update so a subsequent
|
|
262
|
+
# parse_object call picks up the modified payload state.
|
|
263
|
+
# @!visibility private
|
|
264
|
+
def reset_parse_object_cache!
|
|
265
|
+
@parse_object = nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
def build_parse_object
|
|
271
|
+
# if its a before trigger, then we build the original object and apply the updates
|
|
272
|
+
# in order to create a Parse::Object that has the dirty tracking information
|
|
273
|
+
# if no original is nil, then it means this is a brand new object, so we create
|
|
274
|
+
# one from the className
|
|
275
|
+
if before_trigger?
|
|
276
|
+
# if original is present, then this is a modified object
|
|
277
|
+
if @original.present? && @original.is_a?(Hash)
|
|
278
|
+
o = Parse::Object.build @original, parse_class
|
|
279
|
+
o.apply_attributes! @object, dirty_track: true
|
|
280
|
+
|
|
281
|
+
if o.is_a?(Parse::User) && @update.present? && @update["authData"].present?
|
|
282
|
+
o.auth_data = @update["authData"]
|
|
283
|
+
end
|
|
284
|
+
return o
|
|
285
|
+
else #else the object must be new
|
|
286
|
+
klass = Parse::Object.find_class parse_class
|
|
287
|
+
# if we have a class, return that with updated changes, otherwise
|
|
288
|
+
# default to regular object
|
|
289
|
+
if klass.present?
|
|
290
|
+
o = klass.new(@object || {})
|
|
291
|
+
if o.is_a?(Parse::User) && @update.present? && @update["authData"].present?
|
|
292
|
+
o.auth_data = @update["authData"]
|
|
293
|
+
end
|
|
294
|
+
return o
|
|
295
|
+
end # if klass.present?
|
|
296
|
+
end # if we have original
|
|
297
|
+
end # if before_trigger?
|
|
298
|
+
Parse::Object.build(@object, parse_class)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
public
|
|
302
|
+
|
|
303
|
+
# This method will intentionally raise a {Parse::Webhooks::ResponseError} with
|
|
304
|
+
# a specific message. When used inside of a registered cloud code webhook
|
|
305
|
+
# function or trigger, will halt processing and return the proper error response
|
|
306
|
+
# code back to the Parse server.
|
|
307
|
+
# @param msg [String] the error message to send back.
|
|
308
|
+
# @raise Parse::Webhooks::ResponseError
|
|
309
|
+
# @return [Parse::Webhooks::ResponseError] the raised exception
|
|
310
|
+
def error!(msg = "")
|
|
311
|
+
raise Parse::Webhooks::ResponseError, msg
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @return [Parse::Query] the Parse query for a beforeFind trigger.
|
|
315
|
+
def parse_query
|
|
316
|
+
return nil unless parse_class.present? && @query.is_a?(Hash)
|
|
317
|
+
Parse::Query.new parse_class, @query
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Returns true if this webhook was triggered by a Ruby Parse Stack request.
|
|
321
|
+
# This is determined by checking for the '_RB_' prefix in the request ID header.
|
|
322
|
+
# This flag is useful for preventing callback loops and implementing intelligent
|
|
323
|
+
# callback handling based on the request origin.
|
|
324
|
+
# @return [Boolean] true if the request originated from Ruby Parse Stack
|
|
325
|
+
def ruby_initiated?
|
|
326
|
+
@ruby_initiated ||= begin
|
|
327
|
+
request_id = nil
|
|
328
|
+
|
|
329
|
+
if @raw.respond_to?(:[])
|
|
330
|
+
# Check for headers at the top level first
|
|
331
|
+
request_id = @raw["x-parse-request-id"] || @raw["X-Parse-Request-Id"] ||
|
|
332
|
+
@raw[:x_parse_request_id] || @raw[:'X-Parse-Request-Id']
|
|
333
|
+
|
|
334
|
+
# If not found at top level, check nested headers
|
|
335
|
+
if request_id.nil?
|
|
336
|
+
headers_sym = @raw[:headers] if @raw[:headers].is_a?(Hash)
|
|
337
|
+
headers_str = @raw["headers"] if @raw["headers"].is_a?(Hash)
|
|
338
|
+
|
|
339
|
+
if headers_sym
|
|
340
|
+
request_id = headers_sym["x-parse-request-id"] || headers_sym["X-Parse-Request-Id"]
|
|
341
|
+
elsif headers_str
|
|
342
|
+
request_id = headers_str["x-parse-request-id"] || headers_str["X-Parse-Request-Id"]
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
request_id&.start_with?("_RB_") || false
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Returns true if this webhook was triggered by a client request (JavaScript, iOS, Android, etc.)
|
|
352
|
+
# This is the inverse of ruby_initiated? and is useful for callback logic that should
|
|
353
|
+
# only run for client-initiated operations.
|
|
354
|
+
# @return [Boolean] true if the request originated from a client (not Ruby)
|
|
355
|
+
def client_initiated?
|
|
356
|
+
!ruby_initiated?
|
|
357
|
+
end
|
|
358
|
+
end # Payload
|
|
359
|
+
end
|
|
360
|
+
end
|