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/replay_protection.rb
Overview
Interface to the CloudCode webhooks API.
Defined Under Namespace
Modules: Registration, ReplayProtection Classes: Payload, ResponseError
Constant Summary collapse
- 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.
-
.error(data = false) ⇒ Hash
Generates an error response for Parse Server.
-
.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_function(name, params) ⇒ Object
Run a locally registered webhook function.
-
.success(data = true) ⇒ Hash
Generates a success response for Parse Server.
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
353 354 355 356 |
# File 'lib/parse/webhooks.rb', line 353 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
336 337 338 339 |
# File 'lib/parse/webhooks.rb', line 336 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 if not configured.
319 320 321 322 323 324 |
# File 'lib/parse/webhooks.rb', line 319 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.
111 112 113 |
# File 'lib/parse/webhooks.rb', line 111 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.
366 367 368 369 |
# File 'lib/parse/webhooks.rb', line 366 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.
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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 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 258 259 260 261 262 263 264 265 266 267 268 269 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 |
# File 'lib/parse/webhooks.rb', line 168 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| payload.instance_exec(payload, &hook) }.last else result = payload.instance_exec(payload, ®istry) 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 prepare_result = result.prepare_save! # If prepare_save! returns false (callback chain was halted), throw an error if prepare_result == false raise Parse::Webhooks::ResponseError, "Save halted by before_save callback" 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 # 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 && (result == true || result.nil?) && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object) # Handle after_save callbacks intelligently based on request origin. # For trusted-Ruby-initiated saves (both `_RB_` header AND master # key), Parse Stack's local `run_callbacks :save` will fire # after_create and after_save callbacks after the REST response # returns; firing them again here would double-fire any side # effect (e.g. an `after_save :send_email` would send two emails # per save). For everything else -- client-initiated saves, or a # spoofed `_RB_` from a non-master client -- Parse Stack never had # a chance to run callbacks, so we fire them here. is_new = payload.original.nil? unless trusted_ruby_initiated payload.parse_object.run_after_create_callbacks if is_new payload.parse_object.run_after_save_callbacks end result = true end result end |
.error(data = false) ⇒ Hash
Generates an error response for Parse Server.
311 312 313 |
# File 'lib/parse/webhooks.rb', line 311 def error(data = false) { error: data }.to_json end |
.route(type, className) { ... } ⇒ OpenStruct
Internally registers a route for a specific webhook trigger or function. ‘:before_delete`, `:after_delete` or `:function`.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/parse/webhooks.rb', line 132 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 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`.
117 118 119 120 121 |
# File 'lib/parse/webhooks.rb', line 117 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_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.
155 156 157 158 159 160 |
# File 'lib/parse/webhooks.rb', line 155 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.
304 305 306 |
# File 'lib/parse/webhooks.rb', line 304 def success(data = true) { success: data }.to_json end |