Class: Parse::Webhooks::TriggerAudit

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/webhooks/trigger_audit.rb

Overview

Operator-facing audit that cross-references three sources of truth about a Parse application's trigger logic and reports where they disagree:

  1. Model callbacks — the ActiveModel before_save / after_save / after_create / ... callbacks declared on each Object subclass (app-defined ones, with framework-internal callbacks filtered out by source location).
  2. Local webhook routes — the blocks registered via webhook :before_save { ... } / Parse::Webhooks.route(...), held in routes.
  3. Server triggers — what is actually registered with Parse Server (GET hooks/triggers), so a matching client POST reaches your Rack app.

The non-obvious relationship the audit exists to surface (see webhooks_guide): a model's ActiveModel callbacks only run server-side for non-Ruby clients when BOTH a local route is registered (so the webhook router has a handler) AND the trigger is registered on Parse Server (so it POSTs at all). Declaring after_save :send_email alone does nothing for a JS/Swift/REST/Dashboard write — that write never touches your Ruby process, and the callback is silently skipped.

SECURITY POSTURE — mirrors Core::Describe. This is operator-side observability, NOT data exposed to an LLM. The server fetch hits the master-key-only hooks/triggers endpoint, so a network: true audit requires a master-key client; network: false audits callbacks vs. local routes only and needs no credentials. Output is never included in tool responses or any parse.agent.* notification payload.

Examples:

audit = Parse::Webhooks.trigger_audit            # Hash report (network)
puts Parse::Webhooks.trigger_audit(pretty: true) # human-readable summary
Parse::Webhooks.trigger_audit(network: false)    # local-only, no master key

Defined Under Namespace

Classes: ClassAudit

Constant Summary collapse

OBJECT_TRIGGERS =

The object-shaped triggers an ActiveModel callback or a webhook block can map to. (Auth / LiveQuery triggers carry no object and have no ActiveModel callback equivalent, so they are surfaced only as server/local routes, not cross-referenced against model callbacks.)

%i[
  before_save after_save before_delete after_delete before_find after_find
].freeze
CALLBACK_TRIGGER_MAP =

Maps an ActiveModel callback chain + phase to the local trigger name whose webhook handler runs it server-side. before_create / after_create ride inside the save handler (Parse Server has no create trigger); the webhook router runs the destroy chain inside the beforeDelete handler.

{
  [:save,    :before] => :before_save,
  [:create,  :before] => :before_save,
  [:save,    :after]  => :after_save,
  [:create,  :after]  => :after_save,
  [:destroy, :before] => :before_delete,
  [:destroy, :after]  => :after_delete,
}.freeze
LOCAL_ONLY_MAP =

ActiveModel callback chains + phases with NO server trigger that can run them. The webhook router only runs the :save and :create chains (plus the destroy chain on beforeDelete) — it never runs :update or :validation. So these callbacks are LOCAL-ONLY: they fire for Ruby-initiated saves but can never fire for a non-Ruby client, and no trigger registration changes that. Surfaced as an informational note, not a fixable gap.

{
  [:update,     :before] => :before_update,
  [:update,     :after]  => :after_update,
  [:validation, :before] => :before_validation,
  [:validation, :after]  => :after_validation,
}.freeze
CALLBACK_CHAINS =

The ActiveModel callback chains we introspect.

%i[validation create update save destroy].freeze
GEM_PARSE_DIR =

Directory under which a callback's source file marks it as framework-internal (defined by the gem) rather than app-defined. Computed from this file's own location: __dir__ is <gem>/lib/parse/webhooks, so its parent is <gem>/lib/parse.

::File.expand_path("..", __dir__)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(network: true, client: nil, include_framework: false) ⇒ TriggerAudit

Returns a new instance of TriggerAudit.

Parameters:

  • network (Boolean) (defaults to: true)

    when true, query Parse Server for registered triggers (requires a master-key client). When false, audit model callbacks against local routes only.

  • client (Parse::Client, nil) (defaults to: nil)

    optional client override for the server fetch.

  • include_framework (Boolean) (defaults to: false)

    when true, also report gem-internal callbacks (e.g. the _User default-ACL callback). Off by default to keep the report focused on app-defined logic.



146
147
148
149
150
151
152
# File 'lib/parse/webhooks/trigger_audit.rb', line 146

def initialize(network: true, client: nil, include_framework: false)
  @networked         = network
  @include_framework = include_framework
  @client            = client
  @server_lookup     = network ? fetch_server_triggers : {}
  @classes           = build_classes
end

Instance Attribute Details

#classesArray<ClassAudit> (readonly)

Returns one row per audited class, sorted by name.

Returns:



134
135
136
# File 'lib/parse/webhooks/trigger_audit.rb', line 134

def classes
  @classes
end

#networkedBoolean (readonly)

Returns whether the server was queried for registered triggers.

Returns:

  • (Boolean)

    whether the server was queried for registered triggers.



136
137
138
# File 'lib/parse/webhooks/trigger_audit.rb', line 136

def networked
  @networked
end

Instance Method Details

#gapsArray<Hash>

Returns every finding across all classes, flattened, with the class name folded into each entry. Convenient for programmatic checks (CI fails the build if gaps.any? { |g| g[:kind] == :callbacks_inert }).

Returns:

  • (Array<Hash>)

    every finding across all classes, flattened, with the class name folded into each entry. Convenient for programmatic checks (CI fails the build if gaps.any? { |g| g[:kind] == :callbacks_inert }).



157
158
159
160
161
# File 'lib/parse/webhooks/trigger_audit.rb', line 157

def gaps
  @classes.flat_map do |ca|
    ca.findings.map { |f| f.merge(parse_class: ca.parse_class) }
  end
end

#prettyString Also known as: to_s

Returns a human-readable, puts-friendly summary in the style of Model.describe(pretty: true).

Returns:

  • (String)

    a human-readable, puts-friendly summary in the style of Model.describe(pretty: true).



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
# File 'lib/parse/webhooks/trigger_audit.rb', line 186

def pretty
  lines = ["Parse trigger audit (#{networked ? "server-compared" : "local-only"}):"]
  @classes.each do |ca|
    header = "  #{ca.parse_class}"
    header += " [server-only]" unless ca.modeled
    lines << header

    ca.callbacks.each do |trigger, cbs|
      names = cbs.map { |c| c[:name] }.join(", ")
      lines << "    callback #{trigger}: #{names}"
    end
    lines << "    routes:  #{ca.local_routes.map(&:to_s).sort.join(", ")}" if ca.local_routes.any?
    if networked && ca.server_triggers.any?
      lines << "    server:  #{ca.server_triggers.keys.map(&:to_s).sort.join(", ")}"
    end

    if ca.findings.empty?
      lines << "    ok"
    else
      ca.findings.each { |f| lines << "    #{finding_glyph(f[:kind])} #{f[:message]}" }
    end
  end
  s = summary
  lines << ""
  lines << "Summary: #{s[:classes_audited]} class(es), " \
           "#{s[:classes_with_issues]} with issues."
  s[:findings].sort.each { |kind, n| lines << "  #{kind}: #{n}" }
  lines.join("\n")
end

#summaryHash

Returns finding counts keyed by kind, plus class totals.

Returns:

  • (Hash)

    finding counts keyed by kind, plus class totals.



174
175
176
177
178
179
180
181
182
# File 'lib/parse/webhooks/trigger_audit.rb', line 174

def summary
  counts = Hash.new(0)
  gaps.each { |g| counts[g[:kind]] += 1 }
  {
    classes_audited:    @classes.size,
    classes_with_issues: @classes.count(&:issues?),
    findings:           counts,
  }
end

#to_hHash Also known as: as_json

Returns the full JSON-safe report.

Returns:

  • (Hash)

    the full JSON-safe report.



164
165
166
167
168
169
170
# File 'lib/parse/webhooks/trigger_audit.rb', line 164

def to_h
  {
    networked: networked,
    classes:   @classes.map(&:to_h),
    summary:   summary,
  }
end