Module: CurrentScope::GrantDiagnosis

Defined in:
lib/current_scope/grant_diagnosis.rb

Overview

Judges a scoped grant. Two methods, two certainties:

verdict_for      PROVEN — the grant cannot match anything, for any type.
type_untargeted? ADVISORY — suggestive only; the host's runtime hooks
               decide what a controller resolves to, so this can be
               wrong and says so where the operator reads it.

Neither is #90's "inert" (the RECORD is gone; different fix). Design rationale: docs/plans/2026-07-28-032-feat-unresolvable-grant-guardrail-plan.md

Class Method Summary collapse

Class Method Details

.type_untargeted?(grant, verdict: :__unset) ⇒ Boolean

verdict: lets a caller that already has it skip the recompute (three role_permissions plucks per grant otherwise).

Returns:

  • (Boolean)


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
# File 'lib/current_scope/grant_diagnosis.rb', line 41

def type_untargeted?(grant, verdict: :__unset)
  verdict = verdict_for(grant) if verdict == :__unset
  return false unless verdict.nil?
  return false if orphaned?(grant)

  role = grant.role
  return false if role.nil? || role.full_access?

  return false unless ensure_models_loaded!

  klass = resource_class(grant)
  return false if klass.nil? # unresolvable class is #90's inert, not ours

  keys = persisted_keys(role)
  return false if keys.any? { |key| routed?(key) && targets_any_route_key?(key, klass) }

  # #108: a grant on a PARENT legitimately reaches its children, so stay
  # silent when any declared chain reaches this class.
  !reachable_through_declared_chain?(klass, keys)
rescue NameError, ActiveRecord::ActiveRecordError => e
  raise if e.instance_of?(NoMethodError)

  log_degrade(e)
  false
end

.untargeted_caveatObject

The advisory's caveat, centralised for the same reason the proven wording is: the console and the CLI had drifted into two versions of it.



69
70
71
72
73
74
# File 'lib/current_scope/grant_diagnosis.rb', line 69

def untargeted_caveat
  "This is NOT a verdict. Only your current_scope_record hooks decide which " \
  "records a controller resolves to, and that is not knowable statically. A " \
  "controller serving this type under a different name is a false alarm here. " \
  "Check the hook before removing anything."
end

.verdict_fix(verdict) ⇒ Object



84
85
86
87
88
89
90
91
92
# File 'lib/current_scope/grant_diagnosis.rb', line 84

def verdict_fix(verdict)
  case verdict
  when :no_permissions
    "Tick at least one permission on this role, or remove the grant."
  when :unrouted_permissions
    "Every key on this role is absent from the route-derived catalog, so " \
    "nothing can ever gate them. Re-tick the role against current routes."
  end
end

.verdict_for(grant) ⇒ Object

Proven, or nil. Never guesses.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/current_scope/grant_diagnosis.rb', line 14

def verdict_for(grant)
  return nil if orphaned?(grant)

  role = grant.role
  return nil if role.nil? || role.full_access?

  keys = persisted_keys(role)
  # Checked BEFORE the catalog guard: a role with nothing ticked can never
  # match whatever the catalog says, so deferring to "no verdict" here
  # would downgrade a proven finding to an advisory.
  return :no_permissions if keys.empty?

  # An empty catalog means routes are not derived yet, not that every key
  # is dead. Only the unrouted claim depends on it.
  return nil if CurrentScope.catalog.keys.empty?
  return :unrouted_permissions if keys.none? { |key| live?(key, resource_class(grant)) }

  nil
rescue NameError, ActiveRecord::ActiveRecordError => e
  raise if e.instance_of?(NoMethodError)

  log_degrade(e)
  nil
end

.verdict_label(verdict) ⇒ Object

One wording, so the task and the view cannot drift.



77
78
79
80
81
82
# File 'lib/current_scope/grant_diagnosis.rb', line 77

def verdict_label(verdict)
  case verdict
  when :no_permissions       then "role ticks no permissions"
  when :unrouted_permissions then "role ticks only unrouted keys"
  end
end