Class: Parse::Webhooks

Inherits:
Object
  • Object
show all
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

Registration::ALLOWED_HOOKS

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registration

register_functions!, register_triggers!, register_webhook!, remove_all_functions!, remove_all_triggers!

Methods included from Client::Connectable

#client

Class Attribute Details

.allow_private_webhook_urlsObject



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_unauthenticatedObject



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

.keyString

Returns the configured webhook key if available. By default it will use the value of ENV if not configured.

Returns:



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

.loggingBoolean

Returns whether to print additional logging information. You may also set this to ‘:debug` for additional verbosity.

Returns:

  • (Boolean)

    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.

Parameters:

  • env (Hash)

    the environment hash in a Rack request.

Returns:

Raises:

  • Parse::Webhooks::ResponseError whenever Object, ActiveModel::ValidationError



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.

Parameters:

  • payload (Parse::Webhooks::Payload) (defaults to: nil)

    the payload object received from the server.

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are ‘:before_save`, `:after_save`,

  • className (String)

    if ‘type` is not `:function`, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Returns:

  • (Object)

    the result of the trigger or function.



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, &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
        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.

Parameters:

  • data (Object) (defaults to: false)

    the data to send back with the error.

Returns:

  • (Hash)

    a error data payload



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`.

Parameters:

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are ‘:before_save`, `:after_save`,

  • className (String)

    if ‘type` is not `:function`, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Yields:

  • the block that will handle of the webhook trigger or function.

Returns:

  • (OpenStruct)


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

.routesOpenStruct

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`.

Returns:

  • (OpenStruct)


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.

Returns:

  • (Object)

    the result of the function.



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.

Parameters:

  • data (Object) (defaults to: true)

    the data to send back with the success.

Returns:

  • (Hash)

    a success data payload



304
305
306
# File 'lib/parse/webhooks.rb', line 304

def success(data = true)
  { success: data }.to_json
end

Instance Method Details

#keyString

The Parse Webhook Key to be used for authenticating webhook requests. See key on setting this value.

Returns:



98
99
100
# File 'lib/parse/webhooks.rb', line 98

def key
  self.class.key
end