Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 35s
New adversarial/cross-phase coverage: diamond resource+group hierarchies (deny wins per path), chain inheritance + leaf deny, cycle termination, multi-peer delegation, fact validation, audit snapshot/restore round-trip. Adds acl-validate-facts/acl-facts-valid? (schema) and acl-audit-snapshot/ restore!/copy (audit). Fixed acl-audit-restore! rebuilding the live log via map (append! silently no-ops on map-derived lists). Suite is prover-free: a substrate JIT bug loops the recursive proof reconstructor on deep chains in warm processes (documented in Blockers); acl-permit? is unaffected. 145/145. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.1 KiB
Plaintext
111 lines
3.1 KiB
Plaintext
;; lib/acl/audit.sx — append-only decision log.
|
|
;;
|
|
;; Every decision routed through acl-audit-decide! is appended to an in-memory
|
|
;; log with a monotonic sequence number (no wall-clock — deterministic and
|
|
;; testable; a host can stamp time at the serializer boundary). The log is
|
|
;; append-only: there is no mutate or delete, only append, tail, clear,
|
|
;; snapshot/restore, and serialize-for-disk.
|
|
|
|
(define acl-audit-log (list))
|
|
(define acl-audit-seq 0)
|
|
|
|
;; Copy a list into a fresh, append!-able list. `map`/`rest`-derived lists are
|
|
;; NOT extensible by append! in this runtime (it silently no-ops), so the live
|
|
;; log must always be a list built with `list` + `append!`.
|
|
(define
|
|
acl-audit-copy
|
|
(fn
|
|
(xs)
|
|
(let
|
|
((fresh (list)))
|
|
(do (for-each (fn (e) (append! fresh e)) xs) fresh))))
|
|
|
|
(define
|
|
acl-audit-clear!
|
|
(fn
|
|
()
|
|
(do (set! acl-audit-log (list)) (set! acl-audit-seq 0) nil)))
|
|
|
|
;; Append a decision record. Returns the record.
|
|
(define
|
|
acl-audit-record!
|
|
(fn
|
|
(subj act res allowed?)
|
|
(let
|
|
((entry {:allowed? allowed? :act act :subj subj :res res :seq acl-audit-seq}))
|
|
(do
|
|
(set! acl-audit-seq (+ acl-audit-seq 1))
|
|
(append! acl-audit-log entry)
|
|
entry))))
|
|
|
|
;; Decide against db, log the outcome, and return the boolean. This is the
|
|
;; audited path; acl-permit? remains the pure, side-effect-free decision.
|
|
(define
|
|
acl-audit-decide!
|
|
(fn
|
|
(db subj act res)
|
|
(let
|
|
((allowed? (acl-permit? db subj act res)))
|
|
(do (acl-audit-record! subj act res allowed?) allowed?))))
|
|
|
|
(define acl-audit-count (fn () (len acl-audit-log)))
|
|
|
|
;; Most recent n entries (in chronological order). n >= log size returns all.
|
|
(define
|
|
acl-audit-tail
|
|
(fn
|
|
(n)
|
|
(let
|
|
((total (len acl-audit-log)))
|
|
(if
|
|
(<= total n)
|
|
acl-audit-log
|
|
(acl-audit-drop acl-audit-log (- total n))))))
|
|
|
|
(define
|
|
acl-audit-drop
|
|
(fn
|
|
(xs k)
|
|
(if (<= k 0) xs (acl-audit-drop (rest xs) (- k 1)))))
|
|
|
|
;; Structured snapshot for save/restore — a {:seq :entries} value carrying a
|
|
;; copy of the log (so later appends don't mutate a held snapshot).
|
|
(define acl-audit-snapshot (fn () {:seq acl-audit-seq :entries (acl-audit-copy acl-audit-log)}))
|
|
|
|
;; Replace the live log from a snapshot. Restores both entries and the seq
|
|
;; counter so subsequent records continue numbering correctly. The log is
|
|
;; rebuilt as a fresh append!-able list (see acl-audit-copy).
|
|
(define
|
|
acl-audit-restore!
|
|
(fn
|
|
(snap)
|
|
(do
|
|
(set! acl-audit-log (acl-audit-copy (get snap :entries)))
|
|
(set! acl-audit-seq (get snap :seq))
|
|
nil)))
|
|
|
|
;; Serialize the whole log to a disk-ready string: one record per line,
|
|
;; "seq\tsubj\tact\tres\tallowed?". A host writes this; structured reload is via
|
|
;; snapshot/restore.
|
|
(define
|
|
acl-audit-serialize
|
|
(fn
|
|
()
|
|
(reduce
|
|
(fn
|
|
(acc e)
|
|
(str
|
|
acc
|
|
(get e :seq)
|
|
"\t"
|
|
(get e :subj)
|
|
"\t"
|
|
(get e :act)
|
|
"\t"
|
|
(get e :res)
|
|
"\t"
|
|
(get e :allowed?)
|
|
"\n"))
|
|
""
|
|
acl-audit-log)))
|