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 =
The three canonical Rails write actions — the unambiguous slice of the mutating-action space the writer above warns about.
%w[create update destroy].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.
Constructor Details
#initialize ⇒ Configuration
Returns a new instance of Configuration.
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/current_scope/configuration.rb', line 407 def initialize @user_method = :current_user @actor_method = nil @sod_actions = [] @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.
66 67 68 |
# File 'lib/current_scope/configuration.rb', line 66 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.
122 123 124 |
# File 'lib/current_scope/configuration.rb', line 122 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.
235 236 237 |
# File 'lib/current_scope/configuration.rb', line 235 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.
162 163 164 |
# File 'lib/current_scope/configuration.rb', line 162 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.
345 346 347 |
# File 'lib/current_scope/configuration.rb', line 345 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.
79 80 81 |
# File 'lib/current_scope/configuration.rb', line 79 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.
385 386 387 |
# File 'lib/current_scope/configuration.rb', line 385 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.
83 84 85 |
# File 'lib/current_scope/configuration.rb', line 83 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.
327 328 329 |
# File 'lib/current_scope/configuration.rb', line 327 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].
20 21 22 |
# File 'lib/current_scope/configuration.rb', line 20 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)
135 136 137 |
# File 'lib/current_scope/configuration.rb', line 135 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.
31 32 33 |
# File 'lib/current_scope/configuration.rb', line 31 def sod_identity @sod_identity end |
#subject_class ⇒ Object
Host class acting as the subject, used by the management UI to list assignable subjects.
87 88 89 |
# File 'lib/current_scope/configuration.rb', line 87 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.
105 106 107 |
# File 'lib/current_scope/configuration.rb', line 105 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.
306 307 308 |
# File 'lib/current_scope/configuration.rb', line 306 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.
298 299 300 |
# File 'lib/current_scope/configuration.rb', line 298 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.
288 289 290 |
# File 'lib/current_scope/configuration.rb', line 288 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.
316 317 318 |
# File 'lib/current_scope/configuration.rb', line 316 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.
373 |
# File 'lib/current_scope/configuration.rb', line 373 def report_only? = @enforcement == :report |