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

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 immediate skip_before_action.

Returns:

  • (Boolean)


68
69
70
# File 'lib/standard_id/authorization_bypass.rb', line 68

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.



24
25
26
27
28
29
30
31
32
33
34
35
36
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
# File 'lib/standard_id/authorization_bypass.rb', line 24

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). skip_before_action
    # is idempotent so duplicates are harmless, but this keeps things tidy.
    return if @callback_name

    @callback_name = resolve_callback(framework, callback)
    # @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.



90
91
92
93
94
# File 'lib/standard_id/authorization_bypass.rb', line 90

def apply_skips!
  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.



74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/standard_id/authorization_bypass.rb', line 74

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

  controller.skip_before_action callback, raise: false
  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 (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.



100
101
102
# File 'lib/standard_id/authorization_bypass.rb', line 100

def reset!
  MUTEX.synchronize { @callback_name = nil }
end