Module: Phlex::Reactive::Authorization

Defined in:
lib/phlex/reactive/authorization.rb

Overview

The runtime machinery behind verify_authorized (issue #168): a time-window tracking cell, an interceptor that marks it when a configured authorization method returns, and the enforcement decision the endpoint calls after an action runs.

The design mirrors Phlex::Reactive.with_connection_id: a fiber-local cell (Thread.current, which is fiber-local in Ruby, so Falcon's fiber-per-request model is safe) saved and restored around the action. Because tracking is a WINDOW — anything marking during the action counts — authorization performed in a HELPER the action calls is detected too, not just a top-level call.

Constant Summary collapse

CELL =

The fiber-local key holding the tracking cell during an action. A truthy value means "some authorization method has been called (and returned) during this window".

:phlex_reactive_authorized

Class Method Summary collapse

Class Method Details

.instrument!(component_class) ⇒ Object

Wrap each configured authorization method the class defines (public OR private, own OR inherited) with a prepended module that calls super and then mark! on a NON-RAISING return — so a denial (which raises) never marks and still propagates to the endpoint's authorization_errors → 403.

Idempotent per class OBJECT (memoized on an ivar the class carries), so a Zeitwerk reload's fresh class re-instruments naturally and a repeated call in one process is a no-op. Re-reads authorization_methods each first call, so an initializer that widened the list is honored.



76
77
78
79
80
81
82
83
84
# File 'lib/phlex/reactive/authorization.rb', line 76

def instrument!(component_class)
  return if component_class.instance_variable_get(:@reactive_authorization_instrumented)

  names = Phlex::Reactive.authorization_methods.select do
    component_class.method_defined?(it) || component_class.private_method_defined?(it)
  end
  MODULE_PREPEND.bind_call(component_class, interceptor_for(names)) if names.any?
  component_class.instance_variable_set(:@reactive_authorization_instrumented, true)
end

.mark!Object

Mark the current window authorized. Called by the interceptor (on a non-raising authorization-method return) and by Component#mark_authorized! for a bespoke check. A no-op semantically outside a window (the cell is simply overwritten and restored by the nearest with_tracking).



45
46
47
# File 'lib/phlex/reactive/authorization.rb', line 45

def mark!
  Thread.current[CELL] = true
end

.marked?Boolean

Has anything marked the current window?

Returns:

  • (Boolean)


50
51
52
# File 'lib/phlex/reactive/authorization.rb', line 50

def marked?
  Thread.current[CELL] == true
end

.verify!(component_class, action_def) ⇒ Object

The enforcement decision, called by the endpoint AFTER the action runs, INSIDE the transaction (so a raise rolls the mutation back). A no-op unless verify_authorized is on, the action isn't skipped, and nothing marked the window. Otherwise raises AuthorizationNotVerified naming the component#action and the three remedies.



59
60
61
62
63
64
65
# File 'lib/phlex/reactive/authorization.rb', line 59

def verify!(component_class, action_def)
  return unless Phlex::Reactive.verify_authorized
  return if skipped?(component_class, action_def.name)
  return if marked?

  raise Phlex::Reactive::AuthorizationNotVerified, violation_message(component_class, action_def)
end

.with_trackingObject

Open a fresh tracking window, run the block, and restore the previous cell — exactly the with_connection_id save/restore shape, so nesting is correct and a raise still restores. The inner window starts UNMARKED regardless of an outer mark (a fresh action authorizes for itself).



33
34
35
36
37
38
39
# File 'lib/phlex/reactive/authorization.rb', line 33

def with_tracking
  previous = Thread.current[CELL]
  Thread.current[CELL] = false
  yield
ensure
  Thread.current[CELL] = previous
end