identity: session registry — route by id and (subject, client) + SSO fan-out (9 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 50s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 50s
Directory process holding (SessionId, Subject, Client, Pid) rows. Answers the SSO probe lookup(Subject, Client) and the fan-out sessions_for(Subject) (one subject, many clients). Routes only — no grant state, decides nothing. Integration-tested: register a live session, route to it, confirm active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ trap "rm -f $TMPFILE $OUTFILE" EXIT
|
|||||||
SUITES=(
|
SUITES=(
|
||||||
"session|id-session-test-pass|id-session-test-count"
|
"session|id-session-test-pass|id-session-test-count"
|
||||||
"token|id-token-test-pass|id-token-test-count"
|
"token|id-token-test-pass|id-token-test-count"
|
||||||
|
"registry|id-registry-test-pass|id-registry-test-count"
|
||||||
)
|
)
|
||||||
|
|
||||||
cat > "$TMPFILE" << 'EPOCHS'
|
cat > "$TMPFILE" << 'EPOCHS'
|
||||||
@@ -43,12 +44,16 @@ cat > "$TMPFILE" << 'EPOCHS'
|
|||||||
(load "lib/erlang/runtime.sx")
|
(load "lib/erlang/runtime.sx")
|
||||||
(load "lib/identity/session.sx")
|
(load "lib/identity/session.sx")
|
||||||
(load "lib/identity/token.sx")
|
(load "lib/identity/token.sx")
|
||||||
|
(load "lib/identity/registry.sx")
|
||||||
(load "lib/identity/tests/session.sx")
|
(load "lib/identity/tests/session.sx")
|
||||||
(load "lib/identity/tests/token.sx")
|
(load "lib/identity/tests/token.sx")
|
||||||
|
(load "lib/identity/tests/registry.sx")
|
||||||
(epoch 100)
|
(epoch 100)
|
||||||
(eval "(list id-session-test-pass id-session-test-count)")
|
(eval "(list id-session-test-pass id-session-test-count)")
|
||||||
(epoch 101)
|
(epoch 101)
|
||||||
(eval "(list id-token-test-pass id-token-test-count)")
|
(eval "(list id-token-test-pass id-token-test-count)")
|
||||||
|
(epoch 102)
|
||||||
|
(eval "(list id-registry-test-pass id-registry-test-count)")
|
||||||
EPOCHS
|
EPOCHS
|
||||||
|
|
||||||
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||||
|
|||||||
22
lib/identity/registry.sx
Normal file
22
lib/identity/registry.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
;; identity/registry.sx — routes sessions by id and by (subject, client).
|
||||||
|
;;
|
||||||
|
;; The registry is the directory that makes SSO possible: one subject can
|
||||||
|
;; hold many sessions (one per client), and the OAuth machine asks it the
|
||||||
|
;; single question that drives silent login — \"is there a live session
|
||||||
|
;; for this subject + this client?\". It stores (SessionId, Subject,
|
||||||
|
;; Client, Pid) rows and answers:
|
||||||
|
;;
|
||||||
|
;; whereis_session(Id) -> {ok, Pid} | {error, not_found}
|
||||||
|
;; lookup(Subject, Client) -> {ok, Pid} | {error, not_found} (SSO probe)
|
||||||
|
;; sessions_for(Subject) -> {ok, [SessionId, ...]} (fan-out)
|
||||||
|
;;
|
||||||
|
;; The registry only routes — it holds no grant state and decides nothing.
|
||||||
|
;; Liveness of the routed-to session is that session process's own affair.
|
||||||
|
|
||||||
|
(define
|
||||||
|
identity-registry-source
|
||||||
|
"-module(identity_registry).\n\n start() ->\n spawn(fun () -> loop([]) end).\n\n register(Reg, SessionId, Subject, Client, Pid) ->\n Reg ! {register, SessionId, Subject, Client, Pid, self()},\n receive {registry_reply, R} -> R end.\n\n whereis_session(Reg, SessionId) ->\n Reg ! {whereis_session, SessionId, self()},\n receive {registry_reply, R} -> R end.\n\n lookup(Reg, Subject, Client) ->\n Reg ! {lookup, Subject, Client, self()},\n receive {registry_reply, R} -> R end.\n\n sessions_for(Reg, Subject) ->\n Reg ! {sessions_for, Subject, self()},\n receive {registry_reply, R} -> R end.\n\n deregister(Reg, SessionId) ->\n Reg ! {deregister, SessionId, self()},\n receive {registry_reply, R} -> R end.\n\n stop(Reg) ->\n Reg ! {stop, self()},\n receive {registry_reply, R} -> R end.\n\n loop(Entries) ->\n receive\n {register, SessionId, Subject, Client, Pid, From} ->\n From ! {registry_reply, ok},\n loop([{SessionId, Subject, Client, Pid} | remove_id(SessionId, Entries)]);\n {whereis_session, SessionId, From} ->\n From ! {registry_reply, find_id(SessionId, Entries)},\n loop(Entries);\n {lookup, Subject, Client, From} ->\n From ! {registry_reply, find_sc(Subject, Client, Entries)},\n loop(Entries);\n {sessions_for, Subject, From} ->\n From ! {registry_reply, {ok, collect_subject(Subject, Entries)}},\n loop(Entries);\n {deregister, SessionId, From} ->\n From ! {registry_reply, ok},\n loop(remove_id(SessionId, Entries));\n {stop, From} ->\n From ! {registry_reply, ok}\n end.\n\n find_id(_, []) -> {error, not_found};\n find_id(Id, [{Sid, _, _, Pid} | Rest]) ->\n case Sid =:= Id of\n true -> {ok, Pid};\n false -> find_id(Id, Rest)\n end.\n\n find_sc(_, _, []) -> {error, not_found};\n find_sc(Subject, Client, [{_, Su, Cl, Pid} | Rest]) ->\n case Su =:= Subject of\n true ->\n case Cl =:= Client of\n true -> {ok, Pid};\n false -> find_sc(Subject, Client, Rest)\n end;\n false -> find_sc(Subject, Client, Rest)\n end.\n\n collect_subject(_, []) -> [];\n collect_subject(Subject, [{Sid, Su, _, _} | Rest]) ->\n case Su =:= Subject of\n true -> [Sid | collect_subject(Subject, Rest)];\n false -> collect_subject(Subject, Rest)\n end.\n\n remove_id(_, []) -> [];\n remove_id(Id, [{Sid, Su, Cl, Pid} | Rest]) ->\n case Sid =:= Id of\n true -> remove_id(Id, Rest);\n false -> [{Sid, Su, Cl, Pid} | remove_id(Id, Rest)]\n end.")
|
||||||
|
|
||||||
|
(define
|
||||||
|
identity-load-registry!
|
||||||
|
(fn () (erlang-load-module identity-registry-source)))
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"language": "identity",
|
"language": "identity",
|
||||||
"total_pass": 20,
|
"total_pass": 29,
|
||||||
"total": 20,
|
"total": 29,
|
||||||
"suites": [
|
"suites": [
|
||||||
{"name":"session","pass":11,"total":11,"status":"ok"},
|
{"name":"session","pass":11,"total":11,"status":"ok"},
|
||||||
{"name":"token","pass":9,"total":9,"status":"ok"}
|
{"name":"token","pass":9,"total":9,"status":"ok"},
|
||||||
|
{"name":"registry","pass":9,"total":9,"status":"ok"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# identity-on-sx Scoreboard
|
# identity-on-sx Scoreboard
|
||||||
|
|
||||||
**Total: 20 / 20 tests passing**
|
**Total: 29 / 29 tests passing**
|
||||||
|
|
||||||
| | Suite | Pass | Total |
|
| | Suite | Pass | Total |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ✅ | session | 11 | 11 |
|
| ✅ | session | 11 | 11 |
|
||||||
| ✅ | token | 9 | 9 |
|
| ✅ | token | 9 | 9 |
|
||||||
|
| ✅ | registry | 9 | 9 |
|
||||||
|
|
||||||
|
|
||||||
Generated by `lib/identity/conformance.sh`.
|
Generated by `lib/identity/conformance.sh`.
|
||||||
|
|||||||
99
lib/identity/tests/registry.sx
Normal file
99
lib/identity/tests/registry.sx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
;; identity/tests/registry.sx — routing by id and by (subject, client),
|
||||||
|
;; SSO fan-out (one subject, many clients), and integration with live
|
||||||
|
;; session processes routed through the registry.
|
||||||
|
|
||||||
|
(define id-registry-test-count 0)
|
||||||
|
(define id-registry-test-pass 0)
|
||||||
|
(define id-registry-test-fails (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
id-registry-test
|
||||||
|
(fn
|
||||||
|
(name actual expected)
|
||||||
|
(set! id-registry-test-count (+ id-registry-test-count 1))
|
||||||
|
(if
|
||||||
|
(= actual expected)
|
||||||
|
(set! id-registry-test-pass (+ id-registry-test-pass 1))
|
||||||
|
(append! id-registry-test-fails {:name name :expected expected :actual actual}))))
|
||||||
|
|
||||||
|
(define idr-ev erlang-eval-ast)
|
||||||
|
(define idrnm (fn (v) (get v :name)))
|
||||||
|
|
||||||
|
(identity-load-session!)
|
||||||
|
(identity-load-registry!)
|
||||||
|
|
||||||
|
;; ── whereis by session id ────────────────────────────────────────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"registered session is found by id"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n case identity_registry:whereis_session(Reg, s1) of\n {ok, _} -> found;\n {error, _} -> missing\n end"))
|
||||||
|
"found")
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"unknown session id is not_found, not a crash"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Reg = identity_registry:start(),\n case identity_registry:whereis_session(Reg, nope) of\n {ok, _} -> found;\n {error, Why} -> Why\n end"))
|
||||||
|
"not_found")
|
||||||
|
|
||||||
|
;; ── lookup by (subject, client) — the SSO probe ──────────────────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"lookup finds a session for subject+client"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n case identity_registry:lookup(Reg, alice, web) of\n {ok, _} -> found;\n {error, _} -> missing\n end"))
|
||||||
|
"found")
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"lookup is precise: right subject, wrong client misses"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n case identity_registry:lookup(Reg, alice, cli) of\n {ok, _} -> found;\n {error, _} -> missing\n end"))
|
||||||
|
"missing")
|
||||||
|
|
||||||
|
;; ── SSO fan-out: one subject, many clients ───────────────────────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"sessions_for returns all of a subject's sessions"
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n identity_registry:register(Reg, s2, alice, cli, Me),\n identity_registry:register(Reg, s3, bob, web, Me),\n case identity_registry:sessions_for(Reg, alice) of\n {ok, L} -> length(L)\n end")
|
||||||
|
2)
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"sessions_for an unknown subject is empty"
|
||||||
|
(idr-ev
|
||||||
|
"Reg = identity_registry:start(),\n case identity_registry:sessions_for(Reg, ghost) of\n {ok, L} -> length(L)\n end")
|
||||||
|
0)
|
||||||
|
|
||||||
|
;; ── re-register replaces the row for that id (no duplicates) ──────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"re-registering an id does not duplicate it"
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n identity_registry:register(Reg, s1, alice, web, Me),\n case identity_registry:sessions_for(Reg, alice) of\n {ok, L} -> length(L)\n end")
|
||||||
|
1)
|
||||||
|
|
||||||
|
;; ── deregister removes routing ───────────────────────────────────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"deregistered session is no longer found"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n identity_registry:register(Reg, s1, alice, web, Me),\n identity_registry:deregister(Reg, s1),\n case identity_registry:whereis_session(Reg, s1) of\n {ok, _} -> found;\n {error, _} -> missing\n end"))
|
||||||
|
"missing")
|
||||||
|
|
||||||
|
;; ── integration: route to a live session and look it up ──────────
|
||||||
|
|
||||||
|
(id-registry-test
|
||||||
|
"routed-to session answers lookup as active"
|
||||||
|
(idrnm
|
||||||
|
(idr-ev
|
||||||
|
"Me = self(),\n Reg = identity_registry:start(),\n S = identity_session:start(s1, alice, web, Me, infinity),\n identity_registry:register(Reg, s1, alice, web, S),\n {ok, Pid} = identity_registry:lookup(Reg, alice, web),\n case identity_session:lookup(Pid) of\n {ok, {_,_,_,St}} -> St;\n {error, St} -> St\n end"))
|
||||||
|
"active")
|
||||||
|
|
||||||
|
(define
|
||||||
|
id-registry-test-summary
|
||||||
|
(str "registry " id-registry-test-pass "/" id-registry-test-count))
|
||||||
@@ -19,7 +19,7 @@ through the event log, all authorization questions delegated to `acl-on-sx`.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/identity/conformance.sh` → **20/20** (Phase 1: session, token)
|
`bash lib/identity/conformance.sh` → **29/29** (Phase 1: session, token, registry)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke)
|
|||||||
## Phase 1 — Sessions + tokens
|
## Phase 1 — Sessions + tokens
|
||||||
- [x] `session.sx` — session process, create/lookup/expire
|
- [x] `session.sx` — session process, create/lookup/expire
|
||||||
- [x] `token.sx` — issue/introspect/revoke (opaque, grant-backed)
|
- [x] `token.sx` — issue/introspect/revoke (opaque, grant-backed)
|
||||||
- [ ] `registry.sx` — route by subject/client
|
- [x] `registry.sx` — route by subject/client
|
||||||
- [ ] `api.sx` + tests + scoreboard + conformance.sh
|
- [ ] `api.sx` + tests + scoreboard + conformance.sh
|
||||||
|
|
||||||
## Phase 2 — OAuth2 flows
|
## Phase 2 — OAuth2 flows
|
||||||
@@ -78,6 +78,11 @@ lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke)
|
|||||||
- [ ] tests: audit completeness, cross-instance subject mapping
|
- [ ] tests: audit completeness, cross-instance subject mapping
|
||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
- 2026-06-06 — `registry.sx`: directory process routing sessions by id and
|
||||||
|
by (subject, client). Answers the SSO probe `lookup(Subject, Client)` and
|
||||||
|
the fan-out `sessions_for(Subject)` (one subject, many clients). Routes
|
||||||
|
only — holds no grant state. Integration-tested end-to-end: register a live
|
||||||
|
session, route to it, confirm it answers active. +9 → 29/29.
|
||||||
- 2026-06-06 — `token.sx`: opaque grant-backed tokens. Token = `make_ref`
|
- 2026-06-06 — `token.sx`: opaque grant-backed tokens. Token = `make_ref`
|
||||||
(carries no info); the token table is a process; `introspect` is a live
|
(carries no info); the token table is a process; `introspect` is a live
|
||||||
lookup every time so revocation is real (RFC 7009) — a revoked token reads
|
lookup every time so revocation is real (RFC 7009) — a revoked token reads
|
||||||
|
|||||||
Reference in New Issue
Block a user