Class: CurrentScope::Resolver
- Inherits:
-
Object
- Object
- CurrentScope::Resolver
- Defined in:
- lib/current_scope/resolver.rb
Overview
The decision point. Every allow/deny question in the system routes through here, in a fixed order:
1. SoD veto — the record's initiator can never perform an SoD action
on it. Reads two identities: the effective subject and
(under sod_identity :either) the real actor behind an
impersonated session. Overrides everything, incl. full_access.
2. full_access — the subject's org-wide role grants all permissions,
present and future.
3. org-wide role — the role's permission set includes this permission.
4. scoped role — a role held on THIS record grants the permission.
5. default-deny — nothing granted means denied.
Constant Summary collapse
- INITIATOR_METHOD =
:current_scope_initiator- BYPASS_METHOD =
Host-defined per-record opt-in for break-glass. Absent ⇒ never bypassed (fail-closed, no raise — unlike a missing initiator, absence here is unambiguous).
:current_scope_sod_bypassed?
Instance Method Summary collapse
-
#allow?(subject:, permission:, record: nil, actor: nil) ⇒ Boolean
Public contract: boolean.
-
#decide(subject:, permission:, record: nil, actor: nil) ⇒ Object
Internal decision: returns [allowed_bool, reason_or_nil].
- #full_access?(subject) ⇒ Boolean
-
#org_role(subject) ⇒ Object
The subject's one org-wide role.
-
#scope_for(subject:, model:, permission:) ⇒ Object
The list-side complement to allow?: "which records of
modelmay this subject act on?".
Instance Method Details
#allow?(subject:, permission:, record: nil, actor: nil) ⇒ Boolean
Public contract: boolean. actor is the REAL principal behind the
request (defaults to the subject — no impersonation); it only widens the
SoD veto under config.sod_identity == :either.
24 25 26 |
# File 'lib/current_scope/resolver.rb', line 24 def allow?(subject:, permission:, record: nil, actor: nil) decide(subject: subject, permission: , record: record, actor: actor).first end |
#decide(subject:, permission:, record: nil, actor: nil) ⇒ Object
Internal decision: returns [allowed_bool, reason_or_nil]. The reason is a machine-readable cause the Guard surfaces: :sod_veto / :no_grant on a denial, and :sod_bypassed on the one AUDITED allow (break-glass). Ordinary allows carry nil. The resolver — shared across threads — holds no per-decision state; the reason rides in the return tuple, not on self.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/current_scope/resolver.rb', line 33 def decide(subject:, permission:, record: nil, actor: nil) return [ false, :no_grant ] if subject.nil? case sod_decision(subject: subject, actor: actor, permission: , record: record) when :veto then return [ false, :sod_veto ] when :bypass then return [ true, :sod_bypassed ] # break-glass: privileged, audited override end role = org_role(subject) return [ true, nil ] if role&.full_access? return [ true, nil ] if role&.grants?() return [ true, nil ] if scoped_grant?(subject: subject, permission: , record: record) [ false, :no_grant ] end |
#full_access?(subject) ⇒ Boolean
59 60 61 |
# File 'lib/current_scope/resolver.rb', line 59 def full_access?(subject) !!(subject && org_role(subject)&.full_access?) end |
#org_role(subject) ⇒ Object
The subject's one org-wide role. Memoized per request (via Current) so the many gate checks a single request makes don't each re-query — the decision is identical, only the lookup is cached, keeping the resolver a pure decision function over its inputs.
53 54 55 56 57 |
# File 'lib/current_scope/resolver.rb', line 53 def org_role(subject) CurrentScope::Current.memoized_org_role(subject) do RoleAssignment.find_by(subject: subject)&.role end end |
#scope_for(subject:, model:, permission:) ⇒ Object
The list-side complement to allow?: "which records of model may this
subject act on?". Reads the SAME org + scoped grants the gate reads, so a
host list can never drift from the per-record decision. Fail-closed (nil
subject / no grant → none) and flat — no parent/child cascade. SoD does
NOT apply: it vetoes record-targeted actions, not list membership.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/current_scope/resolver.rb', line 68 def scope_for(subject:, model:, permission:) return model.none if subject.nil? role = org_role(subject) return model.all if role&.full_access? || role&.grants?() # Records on which the subject holds a scoped role that grants the key. # Query the polymorphic base_class (what scoped grants store), not the # passed model's name — otherwise scope_for(STISubclass) returns nothing # while the per-record gate (also keyed on base_class) would allow it. The # `model.where` still applies STI's own type predicate, so a subclass query # can't over-list sibling-subclass rows. An empty subquery yields an empty # (still chainable) relation. model.where( id: ScopedRoleAssignment .where(subject: subject, resource_type: model.base_class.name, role_id: roles_granting()) .select(:resource_id) ) end |