Class: CurrentScope::Configuration
- Inherits:
-
Object
- Object
- CurrentScope::Configuration
- Defined in:
- lib/current_scope/configuration.rb
Constant Summary collapse
- SOD_IDENTITY_MODES =
%i[either subject].freeze
- PROD_MUTATIONS_ENV =
Env var that opts a PRODUCTION deploy into impersonated writes. The VALUE is what counts: a truthy value ("1", "true", "yes"…) lifts the production refusal; unset, "", "false", "0", "off" all mean "not opted in" — an operator writing
…=falsein a deploy manifest gets what they said, not the opposite. development/test/staging never consult it. "CURRENT_SCOPE_ALLOW_PROD_IMPERSONATION_MUTATIONS"- MUTATING_ACTION_NAMES =
Canonical mutating / bulk-write action names the collection_read writer warns about. Includes destroy_all/update_all — the docs' escalation examples — so those cannot reintroduce #49 via config with no signal. Custom names still evade any partial blocklist; that ceiling is deliberate.
%w[create update destroy destroy_all update_all].freeze
- AUDIT_MODES =
[ false, true, :strict ].freeze
- ENFORCEMENT_MODES =
%i[enforce report].freeze
- GATING_TRIPWIRE_MODES =
%i[raise warn].freeze
Instance Attribute Summary collapse
-
#actor_method ⇒ Object
Host controller method returning the REAL actor while impersonating (the pretender's true_user).
-
#allow_mutations_while_impersonating ⇒ Object
When false (the default), an impersonated session is read-only: any non-GET/HEAD request is denied while a real actor acts as a different subject — INCLUDING the engine's own management UI.
-
#allow_sod_bypass ⇒ Object
Break-glass override for the SoD veto.
-
#audit ⇒ Object
Tri-state: false | true (default) | :strict — controls CurrentScope::Event.record!.
-
#collection_read_actions ⇒ Object
Action names whose RECORD-LESS gate derives its answer from the scoped list (#65): for these, the check asks scope_for — the same id-narrowed query the list renders from — so a scoped full_access grant opens exactly the collections that would show it records, and gate and list agree by construction.
-
#enforcement ⇒ Object
:enforce (default) | :report — what the gate DOES with a denial.
-
#excluded_controllers ⇒ Object
Regexps matched against controller paths to keep infrastructure controllers out of the permission grid.
-
#gating_tripwire ⇒ Object
:raise | :warn — what the opt-in GatingTripwire mixin (A4) does when it catches an action that completed without running the gate.
-
#parent_controller ⇒ Object
Class the management UI's controllers inherit from, so they pick up the host's authentication and layout.
-
#permission_grid_groups ⇒ Object
How the role-editor grid folds RESTful actions into columns.
-
#sod_actions ⇒ Object
Action names subject to the separation-of-duties veto: whoever initiated a record can never perform these actions on it.
-
#sod_bypass_permission ⇒ Object
The grantable permission the record's initiator must hold for a break-glass bypass to lift the veto (default "bypass_sod").
-
#sod_identity ⇒ Object
Which identities the separation-of-duties veto weighs: :either (default) — veto if the effective subject OR (while impersonating) the REAL actor initiated the record, so impersonation can never approve the actor's own record.
-
#subject_class ⇒ Object
Host class acting as the subject, used by the management UI to list assignable subjects.
-
#subject_label ⇒ Object
How a subject is identified in the management UI (subjects table, picker, bulk bar).
-
#user_method ⇒ Object
Host controller method that returns the authenticated subject.
-
#warn_on_cross_controller_derivation ⇒ Object
CurrentScope.permission_key logs a nudge when short-form derivation (
allowed_to?(:show, report)) resolves to a DIFFERENT key than the one the current controller's gate enforces — the documented namespaced/custom-named controller foot-gun. -
#warn_on_inert_scoped_grant ⇒ Object
The gate logs a nudge when a request is DENIED :no_grant, the controller declared no current_scope_record hook at all, and the subject holds a scoped grant that WOULD have applied had the hook returned the record.
-
#warn_on_nil_sod_record ⇒ Object
The gate logs a nudge when an SoD action is gated with no record and the request is ALLOWED — i.e.
-
#warn_on_undeclared_collection_model ⇒ Object
The gate logs a nudge when a request is DENIED :model_undeclared — a declared collection action (current_scope_record returned nil) whose controller names no current_scope_model, while the subject holds a scoped grant ticking the key.
Instance Method Summary collapse
-
#initialize ⇒ Configuration
constructor
A new instance of Configuration.
-
#report_only? ⇒ Boolean
True only in report mode.
-
#sod_bypass_action ⇒ Object
Action segment of sod_bypass_permission — bare name or "controller#action".
-
#sod_bypass_permission_conflicts_with_sod_actions? ⇒ Boolean
True when the break-glass bypass permission is also listed in sod_actions.
-
#validate! ⇒ Object
Boot-time config invariants.
Constructor Details
#initialize ⇒ Configuration
Returns a new instance of Configuration.
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/current_scope/configuration.rb', line 440 def initialize @user_method = :current_user @actor_method = nil @sod_actions = [].freeze @sod_identity = :either @allow_sod_bypass = false @sod_bypass_permission = "bypass_sod" @collection_read_actions = [ "index" ].freeze @allow_mutations_while_impersonating = false @excluded_controllers = [ %r{\Arails/}, %r{\Aactive_storage/}, %r{\Aaction_mailbox/}, %r{\Aturbo/}, %r{\Acurrent_scope/} ] @parent_controller = "::ApplicationController" @subject_class = "User" @audit = true @enforcement = :enforce # Reuses diagnostics_default_on? for its env split and bare-Ruby safety # ONLY — do NOT carry over the diagnostics flags' emit/silence reading. # There, false means stay quiet; here, false means :warn, which EMITS. # The inversion is deliberate: the tripwire mixin is opt-in, so a # production host that included it is asking for the ungated inventory — # the env decides only whether a hit 500s (dev/test) or logs (elsewhere). @gating_tripwire = diagnostics_default_on? ? :raise : :warn @warn_on_nil_sod_record = diagnostics_default_on? @warn_on_inert_scoped_grant = diagnostics_default_on? @warn_on_cross_controller_derivation = diagnostics_default_on? @warn_on_undeclared_collection_model = diagnostics_default_on? @permission_grid_groups = { "read" => %w[index show], "create" => %w[new create], "update" => %w[edit update], "destroy" => %w[destroy] } end |
Instance Attribute Details
#actor_method ⇒ Object
Host controller method returning the REAL actor while impersonating (the pretender's true_user). nil means actor == user (no impersonation), so the actor is never resolved and falls back to the subject.
9 10 11 |
# File 'lib/current_scope/configuration.rb', line 9 def actor_method @actor_method end |
#allow_mutations_while_impersonating ⇒ Object
When false (the default), an impersonated session is read-only: any non-GET/HEAD request is denied while a real actor acts as a different subject — INCLUDING the engine's own management UI. The host's stop-impersonation, sign-out, and sign-in endpoints must opt out with skip_before_action :current_scope_mutation_guard!, or impersonation can never end. The gate runs BEFORE the permission check, so sod_identity is only observable once mutations are allowed (or on a GET-listed sod_action).
Setting this true is refused in production unless the env opt-in below is set — letting a real actor write as someone else is a privilege-escalation and audit-integrity risk, so a prod deploy must acknowledge it explicitly. See the custom writer.
96 97 98 |
# File 'lib/current_scope/configuration.rb', line 96 def allow_mutations_while_impersonating @allow_mutations_while_impersonating end |
#allow_sod_bypass ⇒ Object
Break-glass override for the SoD veto. Default false — OFF preserves v0.1
exactly: the separation-of-duties veto is absolute and this hook is never
consulted. When true, the veto is lifted for a record ONLY when all three
hold, re-checked live at decision time: this flag is on, the record's
current_scope_sod_bypassed? hook returns true, AND the record's initiator
holds the bypass permission (sod_bypass_permission). Every lifted veto is
recorded by the engine (sod.bypassed) and surfaced on
X-Current-Scope-Reason.
HONEST FRAMING: this converts SoD from a structural guarantee into an audited policy override — it's break-glass, not SoD. Its legitimacy rests on being default-off, privilege-gated, and always audited. Unlike allow_mutations_while_impersonating there is NO production env-gate: the feature is per-record, privilege-scoped, and audited-by-construction, so production is its intended home.
154 155 156 |
# File 'lib/current_scope/configuration.rb', line 154 def allow_sod_bypass @allow_sod_bypass end |
#audit ⇒ Object
Tri-state: false | true (default) | :strict — controls
CurrentScope::Event.record!.
false — record! is a silent no-op; hosts that don't want the ledger
set this and skip the events migration.
true — append a row for every event; if the current_scope_events
table hasn't been migrated yet, degrade gracefully (skip +
warn once), so an existing host never breaks on first mutation.
:strict — an audit-mandatory host: a missing events table RAISES instead
of degrading, so a mutation-wrapping transaction rolls back
rather than committing an unaudited grant. (Impersonation-
boundary events have no mutation to roll back — a raise there
is a loud 500 on a mis-migrated host.)
Read as == :strict, never == true — don't flatten the tri-state.
See the validating writer below — a misspelled :strict must not silently
downgrade an audit-mandatory host to best-effort.
268 269 270 |
# File 'lib/current_scope/configuration.rb', line 268 def audit @audit end |
#collection_read_actions ⇒ Object
Action names whose RECORD-LESS gate derives its answer from the scoped list (#65): for these, the check asks scope_for — the same id-narrowed query the list renders from — so a scoped full_access grant opens exactly the collections that would show it records, and gate and list agree by construction. Matched like sod_actions, on the action segment of the key ("index" in "reports#index").
DEFAULT ["index"] — the #65 fix is on out of the box. Set [] to restore the pre-#65 record-less semantics (never a released posture on their own — #19/#50/#65 ship together): explicit ticks still open type-bound record-less gates, a scoped full_access role opens none, and the gate/list disagreement #65 describes comes back with it.
LIST-NARROWING READS ONLY. The full_access safety argument is that the answer is derived from record ids — WHICH records the subject holds — so it is only sound for actions with a list side. Naming a mutating or non-idempotent action here ("create", "destroy_all") hands scoped full_access holders that action on the whole TYPE off a grant on one record: the #49 escalation, reintroduced by config. Custom read-shaped actions (export, search) are the intended additions.
The declared current_scope_model is trusted here the way current_scope_record already is — a wrong declaration plus a scoped full_access grant of the declared type opens that controller's listed reads, so review the declaration like the record hook.
194 195 196 |
# File 'lib/current_scope/configuration.rb', line 194 def collection_read_actions @collection_read_actions end |
#enforcement ⇒ Object
:enforce (default) | :report — what the gate DOES with a denial. (#37)
The adoption ramp. A host retrofitting this engine onto an existing app has a controller suite that goes red the moment the gate is mounted: it is fail-closed, and nothing is granted yet. Report mode lets them mount the gate, run their app, and read what WOULD have been denied out of the ledger — turning a big-bang cutover into a list of grants to seed.
:enforce — deny means 403. The only production posture.
:report — a MISSING GRANT is logged and allowed through instead. Every
other denial still refuses.
Report mode is NOT an off switch and never reaches the things that would make it one: the separation-of-duties veto still refuses, and the management console answers to its own full-access check rather than this gate, so no enforcement setting can hand out the UI where grants are made.
378 379 380 |
# File 'lib/current_scope/configuration.rb', line 378 def enforcement @enforcement end |
#excluded_controllers ⇒ Object
Regexps matched against controller paths to keep infrastructure controllers out of the permission grid. An excluded controller cannot be granted, so it must also skip the gate (skip_before_action :current_scope_check!) — Guard raises otherwise. Skipping the gate leaves the controller ungated by CurrentScope — protect it with your own authorization (e.g. require_admin!). See docs/SECURITY-CHECKLIST.md.
111 112 113 |
# File 'lib/current_scope/configuration.rb', line 111 def excluded_controllers @excluded_controllers end |
#gating_tripwire ⇒ Object
:raise | :warn — what the opt-in GatingTripwire mixin (A4) does when it catches an action that completed without running the gate.
:raise — fail loudly. Default in development/test: an ungated action is
a hole you want CI to go red on, not one to discover in an audit.
:warn — log once per controller#action and let the response through, so
a real app can inventory its ungated surface without 500ing.
A closed two-mode set, like enforcement — not a boolean, and no :off: not including the mixin is off.
418 419 420 |
# File 'lib/current_scope/configuration.rb', line 418 def gating_tripwire @gating_tripwire end |
#parent_controller ⇒ Object
Class the management UI's controllers inherit from, so they pick up the host's authentication and layout.
115 116 117 |
# File 'lib/current_scope/configuration.rb', line 115 def parent_controller @parent_controller end |
#permission_grid_groups ⇒ Object
How the role-editor grid folds RESTful actions into columns. An ordered Hash of { column_label => [action names] }: ticking a group column grants every routed action in it. The default collapses the seven RESTful verbs into CRUD — new/create and edit/update pair up (the "new"/"edit" actions just render the form for their mutation), index+show read as one. Actions not in any group (e.g. "approve") get their own column. Set to nil (or {}) to show every raw action as its own column instead. Either way the grid renders ALIGNED columns — a controller that doesn't route a column's actions shows a blank cell, never a shifted one.
360 361 362 |
# File 'lib/current_scope/configuration.rb', line 360 def @permission_grid_groups end |
#sod_actions ⇒ Object
Action names subject to the separation-of-duties veto: whoever initiated a record can never perform these actions on it. Not editable in the UI by design — SoD is a structural guarantee, not a preference. Records hit by these actions must define current_scope_initiator (return nil to exempt a record type).
EMPTY BY DEFAULT — SoD is opt-in. The engine's baseline is scoped RBAC;
many hosts want nothing to do with four-eyes. Enable it by listing the
actions to gate, e.g. config.sod_actions = %w[approve].
Matched as STRINGS against the key's action segment (see Resolver#sod_action?). Use the writer below — a plain Symbol list ([:approve]) used to silently disable the veto (#91). Same shape as collection_read_actions=.
24 25 26 |
# File 'lib/current_scope/configuration.rb', line 24 def sod_actions @sod_actions end |
#sod_bypass_permission ⇒ Object
The grantable permission the record's initiator must hold for a break-glass bypass to lift the veto (default "bypass_sod"). Resolved against the record's route key like any permission, so it's editable in the role grid — never a hardcoded role. Must NOT be listed in sod_actions (it isn't an SoD action; keeping it out also bounds the bypass re-entrancy).
It isn't a routable action, so the catalog injects it rather than deriving it: the grid shows a column for it ONLY when allow_sod_bypass is on, and only on controllers that route an action listed in sod_actions. With the flag off it isn't grantable at all — nothing to tick, and the key is rejected if assigned. (#21)
167 168 169 |
# File 'lib/current_scope/configuration.rb', line 167 def @sod_bypass_permission end |
#sod_identity ⇒ Object
Which identities the separation-of-duties veto weighs:
:either (default) — veto if the effective subject OR (while
impersonating) the REAL actor initiated the record,
so impersonation can never approve the actor's own
record.
:subject — veto only on the effective subject.
The two are identical when not impersonating (actor == subject). See the validating writer below — an unknown value must not silently narrow the veto.
35 36 37 |
# File 'lib/current_scope/configuration.rb', line 35 def sod_identity @sod_identity end |
#subject_class ⇒ Object
Host class acting as the subject, used by the management UI to list assignable subjects.
119 120 121 |
# File 'lib/current_scope/configuration.rb', line 119 def subject_class @subject_class end |
#subject_label ⇒ Object
How a subject is identified in the management UI (subjects table, picker, bulk bar). A subject id is meaningless with UUID keys, so pick something human. Accepts:
- a Symbol — a method on the subject, e.g. :email or :name
- a Proc — subject -> String, e.g. ->(u) { "#{u.first_name} #{u.last_name}" }
- nil (default) — best-effort, people-first: email, else name, else
first+last, else the generic current_scope_label / "Class #id".
A Proc should be TOTAL — it runs for every subject the admin can see,
including ones with nil or blank attributes, so ->(u) { u.email.upcase }
is a trap the first time someone is invited but hasn't filled in an email.
Nothing here is load-bearing enough to break a page over, so a label that
raises (or a Symbol the subject can't answer) degrades to the default chain
for that subject and logs once — it never errors the page and never affects
an authorization decision. If subject labels look wrong, check the log:
a silent fallback is exactly what a broken label looks like from the UI.
137 138 139 |
# File 'lib/current_scope/configuration.rb', line 137 def subject_label @subject_label end |
#user_method ⇒ Object
Host controller method that returns the authenticated subject.
4 5 6 |
# File 'lib/current_scope/configuration.rb', line 4 def user_method @user_method end |
#warn_on_cross_controller_derivation ⇒ Object
CurrentScope.permission_key logs a nudge when short-form derivation
(allowed_to?(:show, report)) resolves to a DIFFERENT key than the one the
current controller's gate enforces — the documented namespaced/custom-named
controller foot-gun. The view then shows a link that 403s, or hides one that
would have worked: the gate and the view disagree, silently, and the symptom
shows up nowhere near the cause.
339 340 341 |
# File 'lib/current_scope/configuration.rb', line 339 def warn_on_cross_controller_derivation @warn_on_cross_controller_derivation end |
#warn_on_inert_scoped_grant ⇒ Object
The gate logs a nudge when a request is DENIED :no_grant, the controller declared no current_scope_record hook at all, and the subject holds a scoped grant that WOULD have applied had the hook returned the record.
That combination is a controller with member actions that forgot the hook. It fails closed — correctly — but the 403 is indistinguishable from "you were never granted this", so the person debugging it goes looking at their grants, which are fine, instead of at their controller, which isn't.
331 332 333 |
# File 'lib/current_scope/configuration.rb', line 331 def warn_on_inert_scoped_grant @warn_on_inert_scoped_grant end |
#warn_on_nil_sod_record ⇒ Object
The gate logs a nudge when an SoD action is gated with no record and the request is ALLOWED — i.e. the veto was skipped because current_scope_record returned nil (or was never declared) on a member action. The gem's #1 foot-gun: the veto silently not running looks identical to the veto passing.
Emitted from the Guard seam (not the shared resolver), so it never fires on advisory allowed_to?/scope_for calls.
321 322 323 |
# File 'lib/current_scope/configuration.rb', line 321 def warn_on_nil_sod_record @warn_on_nil_sod_record end |
#warn_on_undeclared_collection_model ⇒ Object
The gate logs a nudge when a request is DENIED :model_undeclared — a declared collection action (current_scope_record returned nil) whose controller names no current_scope_model, while the subject holds a scoped grant ticking the key. The record-less branch had no type to bind that grant to, so it failed closed (#50) — correctly, but the 403 looks like "never granted" while the fix is one line in the controller. The same flag gates the :model_invalid nudge — the hook was declared but returned something unusable ("Report" for Report); one failure family, one knob.
349 350 351 |
# File 'lib/current_scope/configuration.rb', line 349 def warn_on_undeclared_collection_model @warn_on_undeclared_collection_model end |
Instance Method Details
#report_only? ⇒ Boolean
True only in report mode. A predicate, so callers can't express "not enforcing" — the modes are a closed set, not a boolean.
406 |
# File 'lib/current_scope/configuration.rb', line 406 def report_only? = @enforcement == :report |
#sod_bypass_action ⇒ Object
Action segment of sod_bypass_permission — bare name or "controller#action". Shared by boot validation and the resolver's recursion guard so the two never normalize differently (#40). Uses split("#", -1) like PermissionCatalog#bypass_action: plain split("#").last turns "reports#" into "reports" (trailing empty dropped) and multi-hash values into the wrong last segment — false conflicts or missed recursion guards. Malformed shapes return nil (no false conflict); the catalog still raises loudly when allow_sod_bypass is on and the key is malformed.
505 506 507 508 509 510 |
# File 'lib/current_scope/configuration.rb', line 505 def sod_bypass_action segments = .to_s.split("#", -1) return if segments.empty? || segments.size > 2 || segments.any?(&:blank?) segments.last end |
#sod_bypass_permission_conflicts_with_sod_actions? ⇒ Boolean
True when the break-glass bypass permission is also listed in sod_actions. That pairing would re-enter the SoD step on every bypass check and stack overflow. Never valid in any environment (#40).
515 516 517 518 |
# File 'lib/current_scope/configuration.rb', line 515 def action = sod_bypass_action action.present? && sod_actions.include?(action) end |
#validate! ⇒ Object
Boot-time config invariants. Wired from Engine#after_initialize after the host initializer has finalized both fields. Extensible seam for future multi-field checks; today only the bypass-in-sod_actions recursion rule.
523 524 525 526 527 528 529 530 531 532 |
# File 'lib/current_scope/configuration.rb', line 523 def validate! return unless action = sod_bypass_action raise ConfigurationError, "config.sod_bypass_permission (#{.inspect}) is the " \ "action #{action.inspect}, which is also in config.sod_actions. The bypass " \ "permission must not be an SoD action — it would recurse. Remove " \ "#{action.inspect} from sod_actions." end |