Files
rose-ash/lib/identity/api.sx
giles 3b782eba8a
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
identity: "apps with access" — per-subject active-grant listing (+7 tests)
identity_tokens:grants_for(Subject) lists a subject's active grants as
[{Client, Scope}] (revoked excluded), exposed through the facade as
identity:grants(Subject). Completes the per-subject account-security trio:
sessions (where logged in), grants (which apps have access), history (what
happened). New tests/account.sx. Conformance internal timeout raised to
1200s (22 suites, ~10min — run in background). 229/229.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 05:45:46 +00:00

36 lines
7.1 KiB
Plaintext

;; identity/api.sx — the unified identity service facade.
;;
;; `identity:start()` spawns one coordinator that owns the whole domain:
;; an audit ledger, a grant-backed token table (wired to that ledger), a
;; session registry, and a membership registry. It exposes the
;; whole-domain operations through one door:
;;
;; login / verify / revoke / logout / session_status (sessions + tokens)
;; sessions(Subject) / logout_all(Subject) (subject-wide mgmt)
;; grants(Subject) (apps with access)
;; history(Subject) (audit ledger)
;; enroll / member_status / member_project (membership)
;;
;; Per subject, three views answer \"what does this account look like\":
;; sessions (where it is logged in), grants (which apps have access), and
;; history (what happened). Every grant transition is audited. verify
;; answers IDENTITY only; membership projection reports WHAT a subject is
;; for an app; whether either may do a thing is acl's call.
(define
identity-api-source
"-module(identity).\n\n start() ->\n spawn(fun () ->\n Audit = identity_audit:start(),\n TokReg = identity_tokens:start(Audit),\n SessReg = identity_registry:start(),\n Members = identity_membership:start(),\n loop(TokReg, SessReg, Audit, Members, 1)\n end).\n\n login(Svc, Subject, Client, Scope) ->\n login(Svc, Subject, Client, Scope, infinity).\n\n login(Svc, Subject, Client, Scope, Ttl) ->\n Svc ! {login, Subject, Client, Scope, Ttl, self()},\n receive {identity_reply, R} -> R end.\n\n verify(Svc, Token) ->\n Svc ! {verify, Token, self()},\n receive {identity_reply, R} -> R end.\n\n revoke(Svc, Token) ->\n Svc ! {revoke, Token, self()},\n receive {identity_reply, R} -> R end.\n\n logout(Svc, SessionId) ->\n Svc ! {logout, SessionId, self()},\n receive {identity_reply, R} -> R end.\n\n logout_all(Svc, Subject) ->\n Svc ! {logout_all, Subject, self()},\n receive {identity_reply, R} -> R end.\n\n sessions(Svc, Subject) ->\n Svc ! {sessions, Subject, self()},\n receive {identity_reply, R} -> R end.\n\n grants(Svc, Subject) ->\n Svc ! {grants, Subject, self()},\n receive {identity_reply, R} -> R end.\n\n session_status(Svc, SessionId) ->\n Svc ! {session_status, SessionId, self()},\n receive {identity_reply, R} -> R end.\n\n history(Svc, Subject) ->\n Svc ! {history, Subject, self()},\n receive {identity_reply, R} -> R end.\n\n enroll(Svc, Subject, Tier) ->\n Svc ! {enroll, Subject, Tier, self()},\n receive {identity_reply, R} -> R end.\n\n member_status(Svc, Subject) ->\n Svc ! {member_status, Subject, self()},\n receive {identity_reply, R} -> R end.\n\n member_project(Svc, Subject, App) ->\n Svc ! {member_project, Subject, App, self()},\n receive {identity_reply, R} -> R end.\n\n loop(TokReg, SessReg, Audit, Members, NextId) ->\n receive\n {login, Subject, Client, Scope, Ttl, From} ->\n SessionId = NextId,\n Self = self(),\n S = identity_session:start(SessionId, Subject, Client, Self, Ttl),\n identity_registry:register(SessReg, SessionId, Subject, Client, S),\n identity_audit:record(Audit, Subject, login),\n {ok, Token} = identity_tokens:issue(TokReg, Subject, Client, Scope),\n From ! {identity_reply, {ok, SessionId, Token}},\n loop(TokReg, SessReg, Audit, Members, NextId + 1);\n {verify, Token, From} ->\n From ! {identity_reply, identity_tokens:introspect(TokReg, Token)},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {revoke, Token, From} ->\n identity_tokens:revoke(TokReg, Token),\n From ! {identity_reply, ok},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {logout, SessionId, From} ->\n case identity_registry:whereis_session(SessReg, SessionId) of\n {ok, Pid} ->\n audit_logout(Audit, Pid),\n identity_session:revoke(Pid);\n {error, _} -> ok\n end,\n identity_registry:deregister(SessReg, SessionId),\n From ! {identity_reply, ok},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {logout_all, Subject, From} ->\n case identity_registry:sessions_for(SessReg, Subject) of\n {ok, Ids} -> logout_each(SessReg, Audit, Ids)\n end,\n From ! {identity_reply, ok},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {sessions, Subject, From} ->\n case identity_registry:sessions_for(SessReg, Subject) of\n {ok, Ids} -> From ! {identity_reply, Ids}\n end,\n loop(TokReg, SessReg, Audit, Members, NextId);\n {grants, Subject, From} ->\n From ! {identity_reply, identity_tokens:grants_for(TokReg, Subject)},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {session_status, SessionId, From} ->\n R = case identity_registry:whereis_session(SessReg, SessionId) of\n {ok, Pid} ->\n case identity_session:lookup(Pid) of\n {ok, {_, _, _, St}} -> St;\n {error, St} -> St\n end;\n {error, _} -> gone\n end,\n From ! {identity_reply, R},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {history, Subject, From} ->\n From ! {identity_reply, identity_audit:actions(Audit, Subject)},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {enroll, Subject, Tier, From} ->\n identity_membership:request(Members, Subject, Tier),\n identity_membership:approve(Members, Subject),\n From ! {identity_reply, ok},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {member_status, Subject, From} ->\n From ! {identity_reply, identity_membership:status(Members, Subject)},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {member_project, Subject, App, From} ->\n From ! {identity_reply, identity_membership:project(Members, Subject, App)},\n loop(TokReg, SessReg, Audit, Members, NextId);\n {session_expired, SessionId} ->\n identity_registry:deregister(SessReg, SessionId),\n loop(TokReg, SessReg, Audit, Members, NextId)\n end.\n\n logout_each(_, _, []) -> ok;\n logout_each(SessReg, Audit, [Sid | Rest]) ->\n case identity_registry:whereis_session(SessReg, Sid) of\n {ok, Pid} ->\n audit_logout(Audit, Pid),\n identity_session:revoke(Pid);\n {error, _} -> ok\n end,\n identity_registry:deregister(SessReg, Sid),\n logout_each(SessReg, Audit, Rest).\n\n audit_logout(Audit, Pid) ->\n case identity_session:lookup(Pid) of\n {ok, {_, Subject, _, _}} -> identity_audit:record(Audit, Subject, logout);\n {error, _} -> ok\n end.")
(define identity-load-api! (fn () (erlang-load-module identity-api-source)))
(define
identity-load-all!
(fn
()
(identity-load-session!)
(identity-load-token!)
(identity-load-registry!)
(identity-load-audit!)
(identity-load-membership!)
(identity-load-api!)))