Class: Parse::Webhooks::TriggerAudit
- Inherits:
-
Object
- Object
- Parse::Webhooks::TriggerAudit
- 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:
- 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). - Local webhook routes — the blocks registered via
webhook :before_save { ... }/Parse::Webhooks.route(...), held in routes. - 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.
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_createride 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
:saveand:createchains (plus the destroy chain on beforeDelete) — it never runs:updateor: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.("..", __dir__)
Instance Attribute Summary collapse
-
#classes ⇒ Array<ClassAudit>
readonly
One row per audited class, sorted by name.
-
#networked ⇒ Boolean
readonly
Whether the server was queried for registered triggers.
Instance Method Summary collapse
-
#gaps ⇒ Array<Hash>
Every finding across all classes, flattened, with the class name folded into each entry.
-
#initialize(network: true, client: nil, include_framework: false) ⇒ TriggerAudit
constructor
A new instance of TriggerAudit.
-
#pretty ⇒ String
(also: #to_s)
A human-readable,
puts-friendly summary in the style ofModel.describe(pretty: true). -
#summary ⇒ Hash
Finding counts keyed by kind, plus class totals.
-
#to_h ⇒ Hash
(also: #as_json)
The full JSON-safe report.
Constructor Details
#initialize(network: true, client: nil, include_framework: false) ⇒ TriggerAudit
Returns a new instance of TriggerAudit.
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
#classes ⇒ Array<ClassAudit> (readonly)
Returns one row per audited class, sorted by name.
134 135 136 |
# File 'lib/parse/webhooks/trigger_audit.rb', line 134 def classes @classes end |
#networked ⇒ Boolean (readonly)
Returns 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
#gaps ⇒ Array<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 }).
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 |
#pretty ⇒ String Also known as: to_s
Returns 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 |
#summary ⇒ Hash
Returns 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_h ⇒ Hash Also known as: as_json
Returns 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 |