identity: identity->acl delegation boundary — 401 gates before 403 (+8 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
delegation.sx makes the loop's central rule concrete: check() introspects
the token first — inactive → {error, unauthenticated} (401), acl never
consulted — and only an authenticated subject's request is delegated to
acl, which returns permit/deny ({error, forbidden} = 403). 401 strictly
precedes 403. acl-on-sx (Datalog) is a different SX guest wired at the
integration layer, so the decider here is a labelled stub (permits when
Action in Scope); swap the pid and the boundary is unchanged. New
tests/delegation.sx. 185/185 — extensions backlog clear.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
lib/identity/tests/delegation.sx
Normal file
102
lib/identity/tests/delegation.sx
Normal file
@@ -0,0 +1,102 @@
|
||||
;; identity/tests/delegation.sx — the identity -> acl boundary.
|
||||
;; Authentication (identity) gates BEFORE authorization (acl): an inactive
|
||||
;; token is unauthenticated (401) and acl is never consulted; only an
|
||||
;; authenticated subject's request is delegated to acl for permit/deny.
|
||||
|
||||
(define id-deleg-test-count 0)
|
||||
(define id-deleg-test-pass 0)
|
||||
(define id-deleg-test-fails (list))
|
||||
|
||||
(define
|
||||
id-deleg-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! id-deleg-test-count (+ id-deleg-test-count 1))
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! id-deleg-test-pass (+ id-deleg-test-pass 1))
|
||||
(append! id-deleg-test-fails {:name name :expected expected :actual actual}))))
|
||||
|
||||
(define idl-ev erlang-eval-ast)
|
||||
(define idlnm (fn (v) (get v :name)))
|
||||
|
||||
(identity-load-delegation!)
|
||||
|
||||
;; Shared prelude: a token registry, a stub acl, and a token granting
|
||||
;; [read, write] to alice, all bound.
|
||||
(define
|
||||
idl-setup
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [read, write])")
|
||||
|
||||
;; ── authenticated + acl permits ──────────────────────────────────
|
||||
|
||||
(id-deleg-test
|
||||
"an authenticated, permitted request returns the subject"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
(str
|
||||
idl-setup
|
||||
", case identity_delegation:check(R, A, T, read, doc1) of\n {ok, S} -> S;\n {error, W} -> W\n end")))
|
||||
"alice")
|
||||
|
||||
;; ── authenticated + acl denies → 403 ─────────────────────────────
|
||||
|
||||
(id-deleg-test
|
||||
"an authenticated but unpermitted request is forbidden"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [read]),\n case identity_delegation:check(R, A, T, write, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end"))
|
||||
"forbidden")
|
||||
|
||||
;; ── unauthenticated → 401, acl never consulted ───────────────────
|
||||
|
||||
(id-deleg-test
|
||||
"a revoked token is unauthenticated, not forbidden"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
(str
|
||||
idl-setup
|
||||
", identity_tokens:revoke(R, T),\n case identity_delegation:check(R, A, T, read, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end")))
|
||||
"unauthenticated")
|
||||
|
||||
(id-deleg-test
|
||||
"an unknown token is unauthenticated"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n Bogus = make_ref(),\n case identity_delegation:check(R, A, Bogus, read, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end"))
|
||||
"unauthenticated")
|
||||
|
||||
(id-deleg-test
|
||||
"an expired token is unauthenticated"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [read], 100),\n identity_tokens:advance(R, 100),\n case identity_delegation:check(R, A, T, read, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end"))
|
||||
"unauthenticated")
|
||||
|
||||
;; ── 401 takes precedence over 403 (identity gates first) ─────────
|
||||
|
||||
(id-deleg-test
|
||||
"a revoked token with no matching scope is still unauthenticated"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [admin]),\n identity_tokens:revoke(R, T),\n case identity_delegation:check(R, A, T, read, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end"))
|
||||
"unauthenticated")
|
||||
|
||||
;; ── acl is what decides for an authenticated subject ─────────────
|
||||
|
||||
(id-deleg-test
|
||||
"the same subject is permitted one action and denied another"
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [read]),\n Allowed = case identity_delegation:check(R, A, T, read, doc1) of\n {ok, _} -> 1; {error, _} -> 0 end,\n Denied = case identity_delegation:check(R, A, T, write, doc1) of\n {ok, _} -> 1; {error, _} -> 0 end,\n Allowed - Denied")
|
||||
1)
|
||||
|
||||
(id-deleg-test
|
||||
"identity does not widen permission beyond the token scope"
|
||||
(idlnm
|
||||
(idl-ev
|
||||
"R = identity_tokens:start(),\n A = identity_delegation:stub_acl(),\n {ok, T} = identity_tokens:issue(R, alice, web, [read, write]),\n case identity_delegation:check(R, A, T, delete, doc1) of\n {ok, _} -> permitted;\n {error, W} -> W\n end"))
|
||||
"forbidden")
|
||||
|
||||
(define
|
||||
id-deleg-test-summary
|
||||
(str "delegation " id-deleg-test-pass "/" id-deleg-test-count))
|
||||
Reference in New Issue
Block a user