Class: CurrentScope::Event
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CurrentScope::Event
- Defined in:
- app/models/current_scope/event.rb
Overview
Append-only authorization event ledger. Design rules:
- Append-only: there is no updated_at; persisted rows are never mutated.
- Identities and target are stored as GlobalID strings, alongside a
denormalized `target_label`, so the history outlives the records it
names (a deleted role still renders in the ledger).
- `actor` and `subject` are ALWAYS present. subject == actor unless
impersonating; impersonated rows are exactly the ones where
subject <> actor.
- NORMATIVE target mapping: assignment events (org_role.*, scoped_role.*)
target the GRANTEE (the subject being granted); role.* events target the
role. Role/resource ride in `details`.
The AR-level readonly? ceiling is honest but NOT total. These BYPASS it:
update_all / delete_all / update_column(s) / insert_all / raw SQL.
The sandbox reset job (a later unit) is the one sanctioned delete_all
caller. DB-level hardening is adapter-honest: REVOKE UPDATE/DELETE covers
PostgreSQL and MySQL hosts; SQLite hosts get file permissions only.
Hash-chain tamper-evidence is deferred.
Class Method Summary collapse
-
.record!(event:, target:, details: nil) ⇒ Object
The ONE recording entry point.
Instance Method Summary collapse
-
#readonly? ⇒ Boolean
Once written, a row is immutable — blocks update / save / destroy at the AR layer.
Class Method Details
.record!(event:, target:, details: nil) ⇒ Object
The ONE recording entry point. Reads the ambient actor/subject from CurrentScope::Current, serializes actor/subject/target as GlobalID strings, denormalizes a human label for the target, and — when config.audit is on — appends exactly one row.
CurrentScope::Event.record!(event: "role.created", target: role,
details: { name: "Owner" })
Raises ConfigurationError (loud, matching the SoD posture) when there is no ambient actor. Silent no-op (returns nil) when config.audit is false.
37 38 39 40 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 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'app/models/current_scope/event.rb', line 37 def record!(event:, target:, details: nil) return unless CurrentScope.config.audit actor = CurrentScope::Current.actor if actor.nil? raise CurrentScope::ConfigurationError, "CurrentScope::Event.record! has no actor — CurrentScope::Current.actor is nil. " \ "Set the ambient context (the controller hook, or with_current_user in tests) before recording." end # Current.user is the effective subject; fall back to actor so subject # is never nil (it equals actor whenever not impersonating). subject = CurrentScope::Current.user || actor create!( event: event.to_s, actor: actor.to_gid.to_s, subject: subject.to_gid.to_s, target: target.to_gid.to_s, target_label: label_for(target), details: details, request_id: CurrentScope::Current.request_id ) rescue ActiveRecord::StatementInvalid => e raise unless missing_events_table?(e) # :strict — an audit-mandatory host refuses to commit a mutation with no # audit row. Re-raise so the enclosing (mutation-wrapping) transaction # rolls back rather than silently degrading. Checked as `== :strict`, not # `!= true`, so the tri-state can't be flattened by a future refactor. # (Impersonation-boundary events have no DB mutation to roll back, so a # raise there is simply a loud 500 on a mis-migrated host.) raise if CurrentScope.config.audit == :strict # true (default) — an existing host that upgrades the gem without running # the new migration must not break on its first mutation. Degrade # gracefully: skip recording and warn once (not on every call), naming # the fix. A host that wants audit runs the migration; one that doesn't # can set config.audit = false to silence it, or :strict to fail loud. warn_missing_events_table_once nil end |
Instance Method Details
#readonly? ⇒ Boolean
Once written, a row is immutable — blocks update / save / destroy at the AR layer. See the class header for the operations that bypass this.
24 |
# File 'app/models/current_scope/event.rb', line 24 def readonly? = persisted? |