;; 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))