Class: Parse::Webhooks
- Inherits:
-
Object
- Object
- Parse::Webhooks
- Extended by:
- Registration
- Includes:
- Client::Connectable
- Defined in:
- lib/parse/webhooks.rb,
lib/parse/webhooks/payload.rb,
lib/parse/webhooks/registration.rb,
lib/parse/webhooks/trigger_audit.rb,
lib/parse/webhooks/replay_protection.rb
Overview
Interface to the CloudCode webhooks API.
Defined Under Namespace
Modules: Registration, ReplayProtection Classes: Payload, ResponseError, TriggerAudit
Constant Summary collapse
- AUTH_TRIGGERS =
The authentication-side triggers (local underscore form). These carry a +_User+ / +_Session+ as the payload object but are NOT object save/delete triggers: the router runs no ActiveModel save/create/destroy callbacks for them, and Parse Server ignores their response body.
%i[ before_login after_login after_logout before_password_reset_request ].freeze
- LIVE_QUERY_TRIGGERS =
The LiveQuery triggers (local underscore form). Connection-global or event-scoped; Parse Server ignores their response body. Delivered over an HTTP webhook only in a co-located single-process LiveQuery setup.
%i[before_connect before_subscribe after_event].freeze
- NON_OBJECT_TRIGGERS =
Every trigger whose payload is not an object save/delete/find shape. Parse Server's webhook response handler resolves +{}+ for all of these (the body is ignored), so the router normalizes their handler result to a success no-op rather than serializing a returned object into the response.
(AUTH_TRIGGERS + LIVE_QUERY_TRIGGERS).freeze
- REJECTABLE_NON_OBJECT_TRIGGERS =
The +before*+ subset of NON_OBJECT_TRIGGERS for which a handler can DENY the operation. Parse Server only treats an +error+ response as a rejection -- a +success:false+ body resolves and lets the login / connect / subscribe / reset proceed. So, mirroring the +before_save+ convention, the router converts a +false+ return from one of these into a ResponseError (which serializes to +error+). +error!+ works for any trigger; the +after*+ variants fire after the fact and cannot undo it.
%i[ before_login before_password_reset_request before_connect before_subscribe ].freeze
- HTTP_PARSE_WEBHOOK =
The name of the incoming env containing the webhook key.
"HTTP_X_PARSE_WEBHOOK_KEY"- HTTP_PARSE_APPLICATION_ID =
The name of the incoming env containing the application id key.
"HTTP_X_PARSE_APPLICATION_ID"- CONTENT_TYPE =
The content type that needs to be sent back to Parse server.
"application/json"
Constants included from Registration
Class Attribute Summary collapse
- .allow_private_webhook_urls ⇒ Object
- .allow_unauthenticated ⇒ Object
-
.key ⇒ String
Returns the configured webhook key if available.
-
.logging ⇒ Boolean
Whether to print additional logging information.
Class Method Summary collapse
-
.call(env) ⇒ Array
Standard Rack call method.
-
.call_route(type, className, payload = nil) ⇒ Object
Calls the set of registered webhook trigger blocks or the specific function block.
-
.dispatch_deferred(env, payload)
Run any Payload#after_response callbacks a handler registered, AFTER the response has been produced.
-
.error(data = false) ⇒ Hash
Generates an error response for Parse Server.
-
.invoke_handler(payload, block) ⇒ Object
Evaluate a single registered handler block in the scope of the payload.
-
.route(type, className) { ... } ⇒ OpenStruct
Internally registers a route for a specific webhook trigger or function.
-
.routes ⇒ OpenStruct
A hash-like structure composing of all the registered webhook triggers and functions.
-
.run_after_save_chain(payload)
Fires the chained ActiveModel after_save (and after_create, for a new object) callbacks for an afterSave delivery -- exactly once per request.
-
.run_after_save_phase(obj, phase)
Runs one phase (:after_create or :after_save) of an afterSave object's chained ActiveModel callbacks, swallowing and logging any StandardError so a post-persist callback failure can't crash the webhook endpoint or suppress the sibling phase.
-
.run_function(name, params) ⇒ Object
Run a locally registered webhook function.
-
.success(data = true) ⇒ Hash
Generates a success response for Parse Server.
-
.trigger_audit(pretty: false, network: true, client: nil, include_framework: false) ⇒ Hash, String
Audit trigger logic across all registered classes, cross-referencing model ActiveModel callbacks, locally registered webhook blocks, and the triggers registered on Parse Server.
-
.trigger_class_from_path(path) ⇒ String?
Extract the Parse class name from a webhook request path.
Instance Method Summary collapse
-
#key ⇒ String
The Parse Webhook Key to be used for authenticating webhook requests.
Methods included from Registration
register_functions!, register_triggers!, register_webhook!, remove_all_functions!, remove_all_triggers!
Methods included from Client::Connectable
Class Attribute Details
.allow_private_webhook_urls ⇒ Object
618 619 620 621 |
# File 'lib/parse/webhooks.rb', line 618 def allow_private_webhook_urls return @allow_private_webhook_urls unless @allow_private_webhook_urls.nil? ENV["PARSE_WEBHOOK_ALLOW_PRIVATE_URLS"] == "true" end |
.allow_unauthenticated ⇒ Object
601 602 603 604 |
# File 'lib/parse/webhooks.rb', line 601 def allow_unauthenticated return @allow_unauthenticated unless @allow_unauthenticated.nil? ENV["PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED"] == "true" end |
.key ⇒ String
Returns the configured webhook key if available. By default it will use the value of ENV['PARSE_SERVER_WEBHOOK_KEY'] if not configured.
584 585 586 587 588 589 |
# File 'lib/parse/webhooks.rb', line 584 def key=(value) @key = value # Reset the warn-once flag so a deployment that configures the key # after startup gets a clean state if the key is later cleared. @missing_key_warned = nil end |
.logging ⇒ Boolean
Returns whether to print additional logging information. You may also
set this to :debug for additional verbosity.
142 143 144 |
# File 'lib/parse/webhooks.rb', line 142 def logging @logging end |
Class Method Details
.call(env) ⇒ Array
Standard Rack call method. This method processes an incoming cloud code webhook request from Parse Server, validates it and executes any registered handlers for it. The result of the handler for the matching webhook request is sent back to Parse Server. If the handler raises a ResponseError, it will return the proper error response.
631 632 633 634 |
# File 'lib/parse/webhooks.rb', line 631 def call(env) # Thraed safety dup.call!(env) end |
.call_route(type, className, payload = nil) ⇒ Object
Calls the set of registered webhook trigger blocks or the specific function block. This method is usually called when an incoming request from Parse Server is received.
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/parse/webhooks.rb', line 309 def call_route(type, className, payload = nil) type = type.to_s.underscore.to_sym #support camelcase className = className.parse_class if className.respond_to?(:parse_class) className = className.to_s return unless routes[type].present? && routes[type][className].present? registry = routes[type][className] # Track the header-derived ruby_initiated flag on the payload so # user code can introspect it (`payload.ruby_initiated?`). For the # framework's own callback-deduplication logic below we use the # stricter `trusted_ruby_initiated`, which additionally requires the # master key. The X-Parse-Request-Id header is client-controllable, # so honoring `_RB_` alone would let any client send `_RB_attacker` # and trick the framework into skipping server-side callbacks. # Server-side Parse-Stack saves use the master key by default, so # the AND is a safe condition for legitimate Ruby-initiated traffic. if payload request_id = payload&.raw&.dig(:headers, "x-parse-request-id") || payload&.raw&.dig("headers", "x-parse-request-id") || payload&.raw&.dig(:headers, "X-Parse-Request-Id") || payload&.raw&.dig("headers", "X-Parse-Request-Id") ruby_initiated = request_id&.start_with?("_RB_") || false payload.instance_variable_set(:@ruby_initiated, ruby_initiated) trusted_ruby_initiated = ruby_initiated && (payload.master? == true) else ruby_initiated = false trusted_ruby_initiated = false end # Pre-block: apply declarative write protection (guard :field, :mode) # to the parse_object that the handler will receive. Running BEFORE # the handler block means trusted server-side writes performed inside # the block are preserved -- only client-supplied values for guarded # fields are reverted. # # Notably we do NOT gate this on ruby_initiated. That flag derives # from a client-controlled X-Parse-Request-Id header, so trusting it # to bypass write protection would allow a one-header attack. Master # key requests still bypass via the master:/payload.master? check. if type == :before_save && payload && payload.object? klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil if klass && klass.respond_to?(:field_guards) && klass.field_guards.any? pre_obj = payload.parse_object # memoized; the handler sees this same instance if pre_obj.respond_to?(:apply_field_guards!) pre_obj.apply_field_guards!( master: payload.master? || false, is_new: payload.original.blank? ) end end end if registry.is_a?(Array) result = registry.map { |hook| invoke_handler(payload, hook) }.last else result = invoke_handler(payload, registry) end if result.is_a?(Parse::Object) # if it is a Parse::Object, we will call the registered ActiveModel callbacks if type == :before_save # returning false from the callback block only runs the before_* callback # Skip prepare_save! when this request is trusted-Ruby-initiated # (both `_RB_` header AND master key), since Parse-Stack already # ran ActiveModel before_save callbacks locally. A client-spoofed # `_RB_` without master falls through and runs them here. unless trusted_ruby_initiated before_save_result = result.run_before_save_callbacks # If a before_save callback halted the chain (returned false), reject the save. if before_save_result == false raise Parse::Webhooks::ResponseError, "Save halted by before_save callback" end # Parse Server exposes no separate beforeCreate trigger, so the # beforeSave hook is the single point at which before_create must # run for a client-initiated create. Run it AFTER before_save, for # new objects only -- matching ActiveModel order (before_save wraps # before_create) and mirroring the afterSave hook, which runs # after_create then after_save. `original.nil?` marks a create. if payload && payload.original.nil? create_result = result.run_before_create_callbacks if create_result == false raise Parse::Webhooks::ResponseError, "Save halted by before_create callback" end end end # For before_save, return the changes payload (what Parse Server expects) result = result.changes_payload elsif type == :before_delete result.run_callbacks(:destroy) { false } result = true end elsif type == :before_save && result == false # If webhook block returns false, halt the save by throwing an error raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook" elsif type == :before_save && (result == true || result.nil?) # Open Source Parse server does not accept true results on before_save hooks. result = {} end # Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/ # afterLogout/beforePasswordResetRequest, beforeConnect/beforeSubscribe/ # afterEvent). Parse Server IGNORES the response body for all of these -- # its webhook response handler resolves {} regardless -- so the ONLY way # a handler can affect the operation is the error path, and only for the # "before" variants (a login/connect/subscribe/reset can be denied; an # after_* fires after the fact and cannot be undone). # # Crucially, Parse Server treats only an {error} response as a rejection: # a {success:false} body RESOLVES and lets the operation proceed. So a # handler that returns `false` to "deny login" would silently allow it. # We mirror the before_save convention and convert that false into a # ResponseError (=> {error} => Parse Server denies). `error!` works for # any of them (the call! rescue converts it). Every other return value -- # including a Parse::Object a handler happened to return (e.g. the _User # from beforeLogin) -- is normalized to a success no-op so we never # serialize an object into the response or the redacted request log. if NON_OBJECT_TRIGGERS.include?(type) if result == false && REJECTABLE_NON_OBJECT_TRIGGERS.include?(type) raise Parse::Webhooks::ResponseError, "#{type} rejected by webhook handler" end result = true end # Guard-injection: when a handler returns a Hash (or true/nil normalized # to {}) for a class with field_guards, Parse Server would otherwise # merge the response with the client's original payload and persist # the client-supplied values for guarded fields. Inject the pre-built # parse_object's changes_payload entries for any guarded field so the # response carries the appropriate revert (Delete op on create, prior # value on update). The Parse::Object return path already runs through # changes_payload on the same memoized instance and therefore needs no # extra injection. if type == :before_save && result.is_a?(Hash) && payload && payload.object? guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any? pre_obj = payload.parse_object # same memoized instance the pre-block step mutated if pre_obj.respond_to?(:changes_payload) guard_payload = pre_obj.changes_payload field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {} guard_klass.field_guards.each_key do |field| remote = (field_map[field.to_sym] || field).to_s result[remote] = guard_payload[remote] if guard_payload.key?(remote) end end end end if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object) # The chained ActiveModel after_save/after_create callbacks are NOT # fired here. `call!` dispatches every trigger twice -- once for the # specific class route and once for the generic `"*"` route -- so # firing the model callbacks inside this per-route block double-fired # them for any app that registered BOTH a class route and a `"*"` # route (e.g. an `after_save :send_email` would send two emails per # save). The dispatch now lives in `run_after_save_chain`, which # `call!` invokes exactly once per delivery after both route calls. # # We still normalize the result to `true` so a handler that returned # the parse_object (the recommended before_save pattern, easy to copy # by mistake) never leaks an object into the response or the log. result = true end result end |
.dispatch_deferred(env, payload)
This method returns an undefined value.
Run any Parse::Webhooks::Payload#after_response callbacks a handler
registered, AFTER the response has been produced. Prefers the server's
rack.after_reply hook (Puma / Unicorn), which fires once the response
is flushed to the socket on the same worker thread; falls back to a
detached thread when the server does not provide it (e.g. WEBrick). Each
callback is isolated so one raising neither aborts the others nor reaches
the client. No-op when nothing was deferred.
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/parse/webhooks.rb', line 270 def dispatch_deferred(env, payload) return if payload.nil? || !payload.respond_to?(:deferred_callbacks) callbacks = payload.deferred_callbacks return if callbacks.blank? runner = proc do callbacks.each do |cb| begin cb.call rescue => e warn "[Webhooks::after_response] deferred callback raised: #{e.class}: #{e.}" end end end # Enqueueing must never break an otherwise-successful response: this runs # just before `response.finish`, so a raise here (a frozen after_reply # array, thread exhaustion) would discard the buffered reply and surface # as a 500. Failing to schedule deferred work degrades to "not run", # never to a failed response. begin after_reply = env.is_a?(Hash) ? env["rack.after_reply"] : nil if after_reply.respond_to?(:<<) after_reply << runner else Thread.new(&runner) end rescue => e warn "[Webhooks::after_response] could not schedule deferred work: #{e.class}: #{e.}" end nil end |
.error(data = false) ⇒ Hash
Generates an error response for Parse Server.
576 577 578 |
# File 'lib/parse/webhooks.rb', line 576 def error(data = false) { error: data }.to_json end |
.invoke_handler(payload, block) ⇒ Object
Evaluate a single registered handler block in the scope of the payload.
The block runs with self bound to the Payload, so a
handler can call parse_object, params, error!, etc. directly --
exactly as it could under the historical payload.instance_exec(payload,
&block) invocation. The difference is the return semantics:
return valuereturnsvalueas the handler result (instead of theLocalJumpError: unexpected returnthat bareinstance_execraised when the block was defined inside a method).- The legacy idioms still work unchanged: the last expression's value,
next value, andbreak valueall returnvalue, andraisepropagates untouched (soerror!/ before_save rejections behave the same).
This is achieved by attaching the block as a singleton method on the
per-request payload (so return gets method semantics) and removing it
afterward. The payload is a per-request instance, so this neither leaks
nor mutates shared state across threads.
Arity is matched to the old instance_exec(payload, ...) contract: a
zero-arity block (do ... end / proc { }) is called with no args; a
block that declares a parameter (do |payload| ... end) or a splat
receives the payload.
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/parse/webhooks.rb', line 233 def invoke_handler(payload, block) name = :"__parse_webhook_handler_#{block.object_id}__" payload.define_singleton_method(name, &block) handler = payload.method(name) begin # Match the old `payload.instance_exec(payload, &block)` arity # leniency: a zero-arity block is called bare; otherwise it receives # the payload, plus a nil for each additional REQUIRED positional so a # block declaring `|payload, extra|` (or more) does not raise — under # instance_exec those surplus params were silently nil. `arity` is # negative for optional/splat params (e.g. -1 for `|*a|`, -2 for # `|a, *b|`); `~arity` gives the required count in that case. if handler.arity == 0 handler.call else required = handler.arity.negative? ? ~handler.arity : handler.arity handler.call(payload, *Array.new([required - 1, 0].max)) end ensure singleton = payload.singleton_class if singleton.method_defined?(name) || singleton.private_method_defined?(name) singleton.send(:remove_method, name) end end end |
.route(type, className) { ... } ⇒ OpenStruct
Internally registers a route for a specific webhook trigger or function.
:before_delete, :after_delete or :function.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/parse/webhooks.rb', line 163 def route(type, className, &block) type = type.to_s.underscore.to_sym #support camelcase if type != :function && className.respond_to?(:parse_class) className = className.parse_class end className = className.to_s # Parse Server has no beforeCreate/afterCreate webhook trigger; the # create variants are ActiveModel callbacks that run inside the # beforeSave/afterSave handler for new objects. Point callers there # rather than registering a route that can never fire. if type == :before_create || type == :after_create save = type == :before_create ? :before_save : :after_save raise ArgumentError, "There is no #{type} webhook. Register `webhook :#{save}` instead — " \ "your #{type} ActiveModel callbacks run inside the #{save} handler " \ "for new objects (registering #{save} enables BOTH the #{save} and " \ "#{type} callbacks)." end if routes[type].nil? || block.respond_to?(:call) == false raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}" end # AfterSave/AfterDelete hooks support more than one if type == :after_save || type == :after_delete routes[type][className] ||= [] routes[type][className].push block else routes[type][className] = block end @routes end |
.routes ⇒ OpenStruct
A hash-like structure composing of all the registered webhook
triggers and functions. These are :before_save, :after_save,
:before_delete, :after_delete or :function.
148 149 150 151 152 |
# File 'lib/parse/webhooks.rb', line 148 def routes return @routes unless @routes.nil? r = Parse::API::Hooks::TRIGGER_NAMES_LOCAL + [:function] @routes = OpenStruct.new(r.reduce({}) { |h, t| h[t] = {}; h }) end |
.run_after_save_chain(payload)
This method returns an undefined value.
Fires the chained ActiveModel after_save (and after_create, for a new object) callbacks for an afterSave delivery -- exactly once per request.
This lives in call! rather than call_route because call! dispatches
every trigger twice (the specific class route AND the generic "*"
route). Firing the model callbacks per-route would double-fire any side
effect for an app that registered both routes. Calling this once, after
both route calls, fires the chain exactly once regardless of how many
routes matched.
The decision to fire depends ONLY on request origin, never on what a
handler returned: Parse Server discards the afterSave response body
entirely, so a handler returning the parse_object must not suppress the
callbacks. For trusted-Ruby-initiated saves (both the _RB_ request-id
header AND the master key) Parse Stack's local run_callbacks :save
already fires these after the REST response returns, so we skip them
here to avoid the double-fire. The route-present guard preserves the
"an unregistered afterSave trigger never fires model callbacks" contract
that call_route's early return used to provide.
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 |
# File 'lib/parse/webhooks.rb', line 498 def run_after_save_chain(payload) return unless payload&.after_save? return unless payload.parse_object.is_a?(Parse::Object) # Preserve the "no registered route => no model callbacks" behavior that # call_route's `return unless routes[type][className].present?` enforced. # Mirror that guard exactly: key on parse_class.to_s (as call_route does) # and use `.present?` on the value -- registration stores an Array, and an # empty/absent registration must NOT fire (matching the original). after_save_routes = routes[:after_save] return unless after_save_routes && (after_save_routes[payload.parse_class.to_s].present? || after_save_routes["*"].present?) # Trusted-Ruby-initiated saves run their callbacks locally; firing again # here would double them. This must match call_route's trusted_ruby_initiated # EXACTLY. call_route runs (and stamps @ruby_initiated) before this for any # matched route, so read that stamped value rather than recomputing via # `ruby_initiated?` -- whose `||=` memoization re-derives on a stamped # `false` and could disagree with call_route's header lookup. return if payload.ruby_initiated? && payload.master? == true # By the time afterSave fires the object is ALREADY persisted in Parse # Server, and Parse Server discards the afterSave response body entirely # (it resolves success even if the handler throws). So a chained callback # that raises must not (a) 500 the webhook endpoint -- `call!`'s rescue # only catches ResponseError / ValidationError, so a bare StandardError # would escape -- nor (b) take out the OTHER phase's unrelated side # effects. Run the after_create and after_save phases independently, each # guarded, logging and swallowing any StandardError. This mirrors Parse's # own afterSave semantics (log-and-continue on a post-persist failure): # a raising `after_create :send_welcome_email` no longer silently skips # an unrelated `after_save :reindex`, and neither can crash the endpoint. obj = payload.parse_object run_after_save_phase(obj, :after_create) if payload.original.nil? run_after_save_phase(obj, :after_save) nil end |
.run_after_save_phase(obj, phase)
This method returns an undefined value.
Runs one phase (:after_create or :after_save) of an afterSave object's
chained ActiveModel callbacks, swallowing and logging any StandardError
so a post-persist callback failure can't crash the webhook endpoint or
suppress the sibling phase. ActiveModel still halts the rest of this
phase's chain on a raise -- only the cross-phase / endpoint blast radius
is contained here. Note this also swallows a ResponseError/ValidationError
raised from inside an after_save callback: afterSave is post-persist and
Parse Server discards the response body, so an error! there cannot deny
the (already-committed) write -- it is logged, not propagated.
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 |
# File 'lib/parse/webhooks.rb', line 549 def run_after_save_phase(obj, phase) case phase when :after_create then obj.run_after_create_callbacks when :after_save then obj.run_after_save_callbacks end nil rescue => e # Redact the exception message before logging: a callback error can echo # record contents/tokens, and the rest of this file routes log output # through the same redactor. warn "[Parse::Webhooks] afterSave #{phase} callback raised for " \ "#{obj.class}##{obj.id} -- the object is already persisted; " \ "logging and continuing: #{e.class}: " \ "#{Parse::Middleware::BodyBuilder.redact(e.)}" nil end |
.run_function(name, params) ⇒ Object
Run a locally registered webhook function. This bypasses calling a function through Parse-Server if the method handler is registered locally.
198 199 200 201 202 203 |
# File 'lib/parse/webhooks.rb', line 198 def run_function(name, params) payload = Payload.new payload.function_name = name payload.params = params call_route(:function, name, payload) end |
.success(data = true) ⇒ Hash
Generates a success response for Parse Server.
569 570 571 |
# File 'lib/parse/webhooks.rb', line 569 def success(data = true) { success: data }.to_json end |
.trigger_audit(pretty: false, network: true, client: nil, include_framework: false) ⇒ Hash, String
Audit trigger logic across all registered classes, cross-referencing model ActiveModel callbacks, locally registered webhook blocks, and the triggers registered on Parse Server. See TriggerAudit.
The server comparison reads the master-key-only hooks/triggers endpoint,
so network: true (the default) requires a master-key client. Pass
network: false for a credential-free audit of callbacks vs. local routes.
494 495 496 497 498 499 |
# File 'lib/parse/webhooks/trigger_audit.rb', line 494 def trigger_audit(pretty: false, network: true, client: nil, include_framework: false) audit = TriggerAudit.new( network: network, client: client, include_framework: include_framework ) pretty ? audit.pretty : audit.to_h end |
.trigger_class_from_path(path) ⇒ String?
Extract the Parse class name from a webhook request path. Parse Server
registers each trigger at <endpoint>/<triggerName>/<className>
(functions at <endpoint>/<functionName>), so for a trigger the class
is the last segment and the second-to-last is a known trigger name.
Returns nil for a function path, a path with no recognizable trigger
segment, or a className that fails the conservative charset check
(Parse class names are [A-Za-z0-9_], built-ins prefixed with _).
The charset gate keeps an attacker-supplied path (reachable when
allow_unauthenticated is set) from injecting an arbitrary routing /
scrub key.
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 |
# File 'lib/parse/webhooks.rb', line 649 def trigger_class_from_path(path) segments = path.to_s.split("/").reject(&:empty?) return nil if segments.size < 2 trigger, klass = segments[-2], segments[-1] # register_triggers! builds the URL with the LOCAL snake_case trigger # name (`after_find`), while Parse Server sends the camelCase form in the # body — accept both so the path segment is recognized either way. known = (Parse::API::Hooks::TRIGGER_NAMES + Parse::API::Hooks::TRIGGER_NAMES_LOCAL).map(&:to_s) return nil unless known.include?(trigger) # Allow a leading `@` for the Parse pseudo-classes (`@Connect` for the # connection-global LiveQuery trigger, `@File` for file triggers): the # SDK encodes the className in the per-trigger URL, so beforeConnect # would not route without it. Mirrors the trigger-className validator # (Parse::API::PathSegment.trigger_class_name!). Still anchored and # charset-limited -- this gate keeps an attacker-supplied path (reachable # only under allow_unauthenticated) from injecting an arbitrary routing # / scrub key. return nil unless /\A@?_?[A-Za-z][A-Za-z0-9_]*\z/.match?(klass) klass end |