Module: StandardId::AuthorizationBypass

Defined in:
lib/standard_id/authorization_bypass.rb

Constant Summary collapse

FRAMEWORK_CALLBACKS =
{
  action_policy: :verify_authorized,
  pundit: :verify_authorized,
  cancancan: :check_authorization
}.freeze
AFTER_ACTION_FRAMEWORKS =

Frameworks where the skip falls through to skip_after_action. ActionPolicy is NOT listed here because it is handled first via CLASS_METHOD_SKIP. Only :pundit reaches this branch.

%i[pundit].freeze
CLASS_METHOD_SKIP =

ActionPolicy provides a dedicated class method to undo verify_authorized. Using skip_before_action or skip_after_action would silently do nothing for ActionPolicy because it manages the callback through its own DSL.

{
  action_policy: :skip_verify_authorized
}.freeze

Class Method Summary collapse

Class Method Details

.applied?Boolean

Whether apply has been called. Used by ControllerPolicy.register to decide if newly loaded controllers need an immediate authorization skip.

Returns:

  • (Boolean)


82
83
84
# File 'lib/standard_id/authorization_bypass.rb', line 82

def applied?
  MUTEX.synchronize { !@callback_name.nil? }
end

.apply(framework: nil, callback: nil) ⇒ Object

Skips the host app’s authorization callback on all engine controllers, and also skips authenticate_account! on public controllers (login, signup, callbacks, etc.) since those must be accessible without a session.

In production (eager_load=true), controllers are already loaded when this runs so the registry is populated. In development (eager_load=false), controllers are loaded lazily on first request; newly registered controllers receive skips immediately via apply_to_controller (called from ControllerPolicy.register). The to_prepare block handles class reloading — after Zeitwerk unloads/reloads classes, the freshly loaded controllers re-register and receive skips again.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/standard_id/authorization_bypass.rb', line 37

def apply(framework: nil, callback: nil)
  if framework && callback
    raise ArgumentError, "Provide framework: or callback:, not both"
  end

  register_prepare = false

  MUTEX.synchronize do
    # Guard against duplicate to_prepare registrations if called more than
    # once (e.g. in tests or misconfigured initializers). The skip methods
    # are idempotent so duplicates are harmless, but this keeps things tidy.
    return if @callback_name

    @callback_name = resolve_callback(framework, callback)
    @framework = framework&.to_sym
    # @prepared is intentionally NOT cleared by reset!. This ensures
    # at most one to_prepare block is registered per process lifetime.
    # Trade-off: after reset! + apply (e.g. in tests switching
    # frameworks), the to_prepare code path is not re-registered, so
    # it can only be verified by the first test that calls apply.
    register_prepare = !@prepared
    @prepared = true
  end

  apply_skips!

  # Re-apply after class reloading in development. In dev (eager_load=false),
  # reset_registry! + apply_skips! is effectively a no-op because the
  # registry is empty at this point — lazy-loaded controllers haven't
  # registered yet. The real work for lazy-loaded controllers is done by
  # apply_to_controller (called from ControllerPolicy.register). This
  # block is still needed because after a Zeitwerk reload, controllers
  # re-register and apply_to_controller fires again for each one, but the
  # reset_registry! here clears stale references to the old class objects
  # to prevent memory leaks in long dev sessions.
  return unless register_prepare

  Rails.application.config.to_prepare do
    StandardId::ControllerPolicy.reset_registry!
    StandardId::AuthorizationBypass.apply_skips!
  end
end

.apply_skips!Object

Must remain public because it is invoked from a to_prepare lambda registered in apply, which executes outside this module’s scope.



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/standard_id/authorization_bypass.rb', line 105

def apply_skips!
  # ControllerPolicy lives in app/controllers/concerns/ and is autoloaded
  # by Zeitwerk. When apply is called early (e.g. from a Rails initializer),
  # the constant may not be loaded yet. This is safe to skip — controllers
  # that register later will receive skips via apply_to_controller (called
  # from ControllerPolicy.register), and the to_prepare block re-runs
  # apply_skips! after class loading is complete.
  return unless defined?(StandardId::ControllerPolicy)

  StandardId::ControllerPolicy.registry_snapshot.each do |policy, controllers|
    controllers.each { |controller| apply_to_controller(controller, policy) }
  end
end

.apply_to_controller(controller, policy) ⇒ Object

Apply skips to a single controller. Called by ControllerPolicy.register when a controller is lazily loaded after apply has already been called.



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/standard_id/authorization_bypass.rb', line 88

def apply_to_controller(controller, policy)
  callback, framework = MUTEX.synchronize { [@callback_name, @framework] }
  return unless callback

  skip_authorization_callback(controller, callback, framework)

  if policy == :public
    # authenticate_account! is defined in WebAuthentication, not on API
    # controllers. raise: false ensures this is a safe no-op for API
    # controllers that don't have the callback.
    controller.skip_before_action :authenticate_account!, raise: false
  end
end

.reset!Object

NOTE: This clears @callback_name and @framework (so applied? returns false and apply can be called again with a different framework) but intentionally does NOT clear @prepared, so no additional to_prepare block is registered.



124
125
126
127
128
129
# File 'lib/standard_id/authorization_bypass.rb', line 124

def reset!
  MUTEX.synchronize do
    @callback_name = nil
    @framework = nil
  end
end