Module: LcpRuby::Authorization::Cache

Defined in:
lib/lcp_ruby/authorization/cache.rb

Overview

Per-request memoization for parent policy classes and resolved parent scopes used by ‘inherits_from`. Storage lives on `LcpRuby::Current`, which Rails resets automatically at request boundaries via ActionDispatch::Executor and at the start of each ActiveJob perform. No manual around_action wiring required.

Pattern: explicit user threading. The cache **does not read** ‘LcpRuby::Current.user`; the user that defines what gets cached is the one passed explicitly to `parent_policy_scope(parent_name, user)`. Cache keys include `user&.id` so impersonated and real views never collide.

Defined Under Namespace

Classes: ScopeError

Constant Summary collapse

RESOLVING =
:__resolving__

Class Method Summary collapse

Class Method Details

.clear!Object

Test/console helper. Production code should rely on Rails’ automatic CurrentAttributes reset at request boundaries.



68
69
70
71
# File 'lib/lcp_ruby/authorization/cache.rb', line 68

def self.clear!
  LcpRuby::Current.authz_scopes = nil
  LcpRuby::Current.authz_policies = nil
end

.parent_policy_scope(parent_name, user) ⇒ Object

Resolves the parent’s policy scope and memoizes per (parent, user). Uses a re-entry sentinel: if a code path triggers re-entry on the same key during its own resolution (cycle escaped validation, DSL flow, dynamic edits) we raise rather than recurse infinitely.

If the underlying resolver raises, the sentinel is cleared so the SAME error surfaces on subsequent calls instead of a misleading “recursion detected” message about the leftover sentinel.



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/lcp_ruby/authorization/cache.rb', line 42

def self.parent_policy_scope(parent_name, user)
  store = (LcpRuby::Current.authz_scopes ||= {})
  key = [ parent_name.to_s, user&.id ]

  existing = store[key]
  if existing == RESOLVING
    raise ScopeError,
          "inheritance recursion detected for parent " \
          "#{parent_name.inspect} (user_id=#{user&.id}); " \
          "inherits_from cycle escaped validation"
  end
  return existing if store.key?(key) && existing != RESOLVING

  store[key] = RESOLVING
  begin
    store[key] = resolve_parent_scope(parent_name, user)
  rescue
    # Roll back the sentinel so a retry surfaces the original error
    # rather than a "recursion detected" red herring.
    store.delete(key)
    raise
  end
end

.policy_for(model_name) ⇒ Object

Per-request memoization of PolicyFactory.policy_for.

NB: PolicyFactory itself has a class-level cache, so this is technically a double cache. The Current-backed layer is kept deliberately for test isolation: ‘Cache.clear!` resets both `authz_scopes` AND `authz_policies` to nil, so test setups that want a clean policy-class slate per example get it via one call instead of also having to remember `PolicyFactory.clear!`. The runtime cost is one hash lookup per request, dwarfed by AR query overhead in any save path.



29
30
31
32
# File 'lib/lcp_ruby/authorization/cache.rb', line 29

def self.policy_for(model_name)
  store = (LcpRuby::Current.authz_policies ||= {})
  store[model_name.to_s] ||= PolicyFactory.policy_for(model_name)
end