persist: worked reference migration — acl grants on persist + 10 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
examples/acl.sx: a tested template migrating an ACL-grants store from a hand-rolled ephemeral map to persist — grants/revokes as events, current set as a projection, O(1) checks via a materialized view, audit via read-window. Header carries the BEFORE->AFTER diff. Proves grants survive restart on the durable backend (the capability the BEFORE version lacked). The pattern other subsystem loops copy; does not touch the real lib/acl. 201/201. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUITES=(event log kv project subscribe concurrency snapshot compaction durable blob view cas catalog query batch upcast idempotency global recovery)
|
||||
SUITES=(event log kv project subscribe concurrency snapshot compaction durable blob view cas catalog query batch upcast idempotency global example-acl recovery)
|
||||
|
||||
OUT_JSON="lib/persist/scoreboard.json"
|
||||
OUT_MD="lib/persist/scoreboard.md"
|
||||
@@ -44,6 +44,7 @@ run_suite() {
|
||||
(load "lib/persist/upcast.sx")
|
||||
(load "lib/persist/idempotency.sx")
|
||||
(load "lib/persist/global.sx")
|
||||
(load "lib/persist/examples/acl.sx")
|
||||
(load "lib/persist/subscribe.sx")
|
||||
(load "lib/persist/api.sx")
|
||||
(epoch 2)
|
||||
|
||||
79
lib/persist/examples/acl.sx
Normal file
79
lib/persist/examples/acl.sx
Normal file
@@ -0,0 +1,79 @@
|
||||
; persist/examples/acl — a WORKED MIGRATION REFERENCE. A subsystem (acl grants:
|
||||
; who may access what) currently hand-rolls an in-memory mutable map that loses
|
||||
; every grant on restart and keeps no audit trail. This shows the same subsystem
|
||||
; rebuilt on persist. It is the template other subsystem loops copy; it does NOT
|
||||
; touch the real lib/acl (out of this loop's scope).
|
||||
;
|
||||
; BEFORE — hand-rolled, ephemeral, no history, no concurrency safety:
|
||||
; (define acl-grants {}) ; resource -> principal list (mutable)
|
||||
; (define acl-grant! (fn (r p) (set! acl-grants (assoc acl-grants r (cons p (get acl-grants r))))))
|
||||
; (define acl-revoke! (fn (r p) (set! acl-grants (assoc acl-grants r (remove p ...)))))
|
||||
; (define acl-can? (fn (r p) (contains? (get acl-grants r) p)))
|
||||
; ;; vanishes on restart; "when/why was X granted?" is unanswerable.
|
||||
;
|
||||
; AFTER — on persist. Grants/revokes are EVENTS (history matters), the current
|
||||
; grant set is a PROJECTION, checks read a materialized VIEW, and the audit trail
|
||||
; is a time-windowed query. Every fn takes a backend `b`, so the same code runs
|
||||
; on the in-memory backend today and the durable backend unchanged.
|
||||
; Requires: lib/persist/log.sx, lib/persist/project.sx, lib/persist/view.sx,
|
||||
; lib/persist/query.sx.
|
||||
|
||||
(define acl/stream (fn (resource) (str "acl/" resource)))
|
||||
|
||||
; write side — grant/revoke append events (the history is the source of truth)
|
||||
(define
|
||||
acl/grant
|
||||
(fn
|
||||
(b resource principal at)
|
||||
(persist/append b (acl/stream resource) "granted" at {:principal principal})))
|
||||
(define
|
||||
acl/revoke
|
||||
(fn
|
||||
(b resource principal at)
|
||||
(persist/append b (acl/stream resource) "revoked" at {:principal principal})))
|
||||
|
||||
; fold step: grant adds a principal (once), revoke removes it
|
||||
(define
|
||||
acl/step
|
||||
(fn
|
||||
(set e)
|
||||
(let
|
||||
((p (get (persist/event-data e) :principal)))
|
||||
(if
|
||||
(equal? (persist/event-type e) "granted")
|
||||
(if (contains? set p) set (append set p))
|
||||
(filter (fn (x) (not (equal? x p))) set)))))
|
||||
|
||||
; read side — current grant set + membership check (replays the log)
|
||||
(define
|
||||
acl/grants
|
||||
(fn
|
||||
(b resource)
|
||||
(persist/project-fold b (acl/stream resource) acl/step (list))))
|
||||
(define
|
||||
acl/can?
|
||||
(fn (b resource principal) (contains? (acl/grants b resource) principal)))
|
||||
|
||||
; materialized view — attach to a hub for O(1) checks that stay current on write
|
||||
(define
|
||||
acl/view
|
||||
(fn
|
||||
(resource)
|
||||
(persist/view
|
||||
(str "acl-current/" resource)
|
||||
(acl/stream resource)
|
||||
acl/step
|
||||
(list))))
|
||||
(define
|
||||
acl/can-fast?
|
||||
(fn
|
||||
(b resource principal)
|
||||
(contains? (persist/view-peek b (acl/view resource)) principal)))
|
||||
|
||||
; audit — grants/revokes for a resource in a time window (the new capability the
|
||||
; hand-rolled version could never answer)
|
||||
(define
|
||||
acl/audit-window
|
||||
(fn
|
||||
(b resource from to)
|
||||
(persist/read-window b (acl/stream resource) from to)))
|
||||
@@ -18,9 +18,10 @@
|
||||
"upcast": {"pass": 9, "fail": 0},
|
||||
"idempotency": {"pass": 9, "fail": 0},
|
||||
"global": {"pass": 11, "fail": 0},
|
||||
"example-acl": {"pass": 10, "fail": 0},
|
||||
"recovery": {"pass": 6, "fail": 0}
|
||||
},
|
||||
"total_pass": 191,
|
||||
"total_pass": 201,
|
||||
"total_fail": 0,
|
||||
"total": 191
|
||||
"total": 201
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@ _Generated by `lib/persist/conformance.sh`_
|
||||
| upcast | 9 | 0 | 9 |
|
||||
| idempotency | 9 | 0 | 9 |
|
||||
| global | 11 | 0 | 11 |
|
||||
| example-acl | 10 | 0 | 10 |
|
||||
| recovery | 6 | 0 | 6 |
|
||||
| **Total** | **191** | **0** | **191** |
|
||||
| **Total** | **201** | **0** | **201** |
|
||||
|
||||
104
lib/persist/tests/example-acl.sx
Normal file
104
lib/persist/tests/example-acl.sx
Normal file
@@ -0,0 +1,104 @@
|
||||
; Reference migration — acl grants on persist. Proves the AFTER behaviour,
|
||||
; including the capabilities the hand-rolled BEFORE version could not provide
|
||||
; (durability across restart + an audit trail).
|
||||
|
||||
(persist-test
|
||||
"grant then can?"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 0)
|
||||
(acl/can? b "doc-1" "alice")))
|
||||
true)
|
||||
(persist-test
|
||||
"no grant means no access"
|
||||
(acl/can? (persist/open) "doc-1" "alice")
|
||||
false)
|
||||
(persist-test
|
||||
"revoke removes access"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 0)
|
||||
(acl/revoke b "doc-1" "alice" 1)
|
||||
(acl/can? b "doc-1" "alice")))
|
||||
false)
|
||||
(persist-test
|
||||
"multiple principals tracked independently"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 0)
|
||||
(acl/grant b "doc-1" "bob" 1)
|
||||
(acl/revoke b "doc-1" "alice" 2)
|
||||
(list (acl/can? b "doc-1" "alice") (acl/can? b "doc-1" "bob"))))
|
||||
(list false true))
|
||||
(persist-test
|
||||
"granting twice is idempotent in the set"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 0)
|
||||
(acl/grant b "doc-1" "alice" 1)
|
||||
(len (acl/grants b "doc-1"))))
|
||||
1)
|
||||
(persist-test
|
||||
"grants on different resources are isolated"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 0)
|
||||
(acl/grant b "doc-2" "bob" 0)
|
||||
(list (acl/can? b "doc-1" "bob") (acl/can? b "doc-2" "bob"))))
|
||||
(list false true))
|
||||
(persist-test
|
||||
"audit window answers when-was-it-granted (new capability)"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(acl/grant b "doc-1" "alice" 100)
|
||||
(acl/revoke b "doc-1" "alice" 200)
|
||||
(acl/grant b "doc-1" "bob" 300)
|
||||
(len (acl/audit-window b "doc-1" 150 300))))
|
||||
2)
|
||||
(persist-test
|
||||
"materialized view stays current on publish"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(let
|
||||
((h (persist/view-attach (persist/hub b) (acl/view "doc-1"))))
|
||||
(begin
|
||||
(persist/publish
|
||||
h
|
||||
(acl/stream "doc-1")
|
||||
"granted"
|
||||
0
|
||||
{:principal "alice"})
|
||||
(acl/can-fast? b "doc-1" "alice"))))
|
||||
true)
|
||||
(persist-test
|
||||
"grants survive restart on the durable backend (the headline win)"
|
||||
(let
|
||||
((disk (persist/mem-backend)))
|
||||
(begin
|
||||
(let
|
||||
((db (persist/mock-durable disk)))
|
||||
(begin
|
||||
(acl/grant db "doc-1" "alice" 0)
|
||||
(acl/grant db "doc-1" "bob" 1)))
|
||||
(let
|
||||
((db2 (persist/mock-durable disk)))
|
||||
(list (acl/can? db2 "doc-1" "alice") (acl/can? db2 "doc-1" "bob")))))
|
||||
(list true true))
|
||||
(persist-test
|
||||
"revoke before restart is still revoked after"
|
||||
(let
|
||||
((disk (persist/mem-backend)))
|
||||
(begin
|
||||
(let
|
||||
((db (persist/mock-durable disk)))
|
||||
(begin
|
||||
(acl/grant db "doc-1" "alice" 0)
|
||||
(acl/revoke db "doc-1" "alice" 1)))
|
||||
(acl/can? (persist/mock-durable disk) "doc-1" "alice")))
|
||||
false)
|
||||
@@ -42,7 +42,7 @@ read models (feeds, indices, audit logs) update incrementally.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/persist/conformance.sh` → **191/191** (Phases 1–4 complete + extensions)
|
||||
`bash lib/persist/conformance.sh` → **201/201** (Phases 1–4 complete + extensions + a reference migration)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -188,7 +188,22 @@ over an in-process disk (the mock-IO harness).
|
||||
feed/-log, flow store, mod/audit, search index, acl grants, identity sessions all
|
||||
become `persist` log or kv. Track each migration in that subsystem's plan.
|
||||
|
||||
**Reference migration:** `lib/persist/examples/acl.sx` is a worked, tested
|
||||
template — an ACL-grants store rebuilt on persist (grants/revokes as events,
|
||||
current set as a projection, O(1) checks via a materialized view, an audit-window
|
||||
query). It carries an explicit BEFORE (hand-rolled ephemeral map) → AFTER
|
||||
diff in its header and proves the headline win (grants survive restart) on the
|
||||
durable backend. Other subsystem loops copy this pattern; it does not touch the
|
||||
real `lib/acl`.
|
||||
|
||||
## Progress log
|
||||
- **Reference migration: acl grants (201/201).** `lib/persist/examples/acl.sx` —
|
||||
a worked, in-scope template migrating an ACL-grants store from a hand-rolled
|
||||
ephemeral map to persist: grants/revokes as events, current set as a
|
||||
projection, O(1) checks via a materialized view, audit via `read-window`.
|
||||
Header carries the BEFORE→AFTER diff. 10 tests, incl. grants surviving restart
|
||||
on the durable backend (the capability the BEFORE version lacked). The pattern
|
||||
other subsystem loops copy.
|
||||
- **Ext: global commit ordering (191/191).** `global.sx` — `persist/gappend`
|
||||
records a pointer in a reserved `$global` index (its seq = global commit
|
||||
position); `read-global`/`project-global` resolve pointers to events in commit
|
||||
|
||||
Reference in New Issue
Block a user