Class: Scryer::AuthorizationWatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/scryer/authorization_watcher.rb

Overview

Runtime companion to the static idor/missing_authorization/ missing_policy_scope rules — those can only ever say "no call to a known authorization method is visible anywhere in this controller's source," which is exactly as wrong as it sounds whenever the real check happens somewhere the static AST walk can't see (a shared base controller, a concern, a class-level macro whose effect isn't visible by name). This watcher answers a narrower but much more reliable question instead: for this actual request, did Pundit's authorize/ policy_scope or CanCanCan's authorize! genuinely get called?

How: both libraries already track this themselves, for their own verify_authorized/check_authorization after_action helpers — Pundit::Authorization#pundit_policy_authorized?/#pundit_policy_scoped? (public API, @_pundit_policy_authorized/@_pundit_policy_scoped under the hood) and CanCanCan's @_authorized ivar (set by authorize! and by skip_authorization_check; verified by reading both gems' actual source, pundit-2.5.2/lib/pundit/authorization.rb and cancancan-3.6.1/lib/cancan/controller_additions.rb — not guessed). This class registers one more after_action, alongside those, that checks the same flags and reports when a write action completed with neither set.

Deliberately Pundit/CanCanCan-only, same scope as the static rules this complements: with neither gem loaded, enable! still runs but every request is silently skipped (see authorization_library_present?) — an app with fully custom, non-object-level authorization (a single before_action :require_admin!, say) gets no findings and no false positives here, rather than a flood of "unauthorized" reports for a pattern this watcher has no way to recognize as intentional.

Deliberately narrower than "check every action": only create/update/ destroy (or any POST/PUT/PATCH/DELETE), matching MissingAuthorizationRule exactly — and only requests that actually completed (status < 400). Read-scoping gaps (an unscoped index — see MissingPolicyScopeRule) are NOT covered here; verifying "was the returned data correctly scoped" at runtime, rather than "was a method called," is a materially different and harder check this class doesn't attempt.

Defined Under Namespace

Classes: Finding

Constant Summary collapse

WRITE_ACTIONS =
%w[create update destroy].freeze
WRITE_METHODS =
%w[POST PUT PATCH DELETE].freeze

Class Method Summary collapse

Class Method Details

.clear!Object



87
88
89
# File 'lib/scryer/authorization_watcher.rb', line 87

def clear!
  @findings = []
end

.enable!(logger: nil) ⇒ Object

Turns the watcher on for the life of the process. Idempotent — safe to call more than once (later calls are no-ops). No Rack middleware to install, unlike QueryWatcher: a Rails controller instance is already fresh per request, so there's no shared/leaking state to scope — the after_action below just runs once per completed action.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/scryer/authorization_watcher.rb', line 55

def enable!(logger: nil)
  return if @enabled

  @logger = logger || default_logger
  @findings = []
  @enabled = true

  # Covers both API-only and normal Rails apps without special-casing
  # either: ActionController::Base and ActionController::API are
  # sibling classes (neither inherits from the other — verified via
  # `ActionController::API.ancestors.include?(ActionController::Base)
  # #=> false`), but Rails' own actionpack source calls
  # `ActiveSupport.run_load_hooks(:action_controller, self)` from
  # *both* action_controller/base.rb and action_controller/api.rb, so
  # this block runs once per base class and `install_hook` ends up
  # registering the after_action on both. Confirmed with a real
  # ActionController::API + Pundit integration test, not assumed.
  ActiveSupport.on_load(:action_controller) { Scryer::AuthorizationWatcher.send(:install_hook, self) }
end

.enabled?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'lib/scryer/authorization_watcher.rb', line 75

def enabled?
  !!@enabled
end

.findingsObject

Every finding recorded so far this process — inspect, log, or feed into your own alerting. Not reset automatically; call clear! yourself (e.g. between test examples, or on a timer) if you don't want it growing for the life of a long-running process.



83
84
85
# File 'lib/scryer/authorization_watcher.rb', line 83

def findings
  @findings ||= []
end