Address whole-project architecture + product reviews (todo §5): make readRoles transitive so group→role grants reach the JWT (matches the Roles 'Effective access' view + OPL model; per-login only), per the user's call; add a zero-JS server-rendered confirm step for delete user/group/role (views/admin/confirm.ejs + shared buildConfirmModel; the Delete control is now a GET link, the delete stays a CSRF-guarded POST); self-lockout guards — no self-delete/deactivate (Users), no self-revoke of the direct admin grant + no delete of the admin role (Roles), each → 400 + inline error (direct-grant paths incl. the seeded admin; group-only-admin lockout = robust last-effective-admin check deferred §9); extract the gate+CSRF preamble copied across the 3 admin handlers into admin-nav.ts requireAdmin/guardedForm; shellUser keeps the email (name = local part, full email beneath). Reviewers: architecture no Critical/High, product 2 Critical + 1 High (all fixed). Deferred (scoped): host route-table→§6, list/template dedup→§5 cleanup, success-flash/empty-states/dangling-refs→§5 polish/§8, safeUrl→§7, 413/https/§N-drift→§9. Tests-first (extended the 3 admin HTTP tests + login/shell-context units); typecheck + 244 units + 8 visual + auth-refresh E2E green; stability-reviewer APPROVE
This commit is contained in:
@@ -409,7 +409,7 @@ session cookie.
|
||||
```
|
||||
── AT LOGIN / REFRESH (the only time Ory is on the path) ──────────
|
||||
Kratos verifies credentials
|
||||
└─► app reads the user's roles from Keto (Keto = source of truth)
|
||||
└─► app reads the user's roles from Keto (direct + transitive via groups)
|
||||
└─► app writes them as a derived projection on the identity (admin API)
|
||||
└─► whoami(tokenize_as: "plainpages") ─► signed JWT
|
||||
claims: { sub, email, roles:[…from Keto], exp ≈ 10m }
|
||||
@@ -432,7 +432,11 @@ and the user can already read these coarse roles in their own JWT, so nothing is
|
||||
That projection is a per-login cache, authoritative nowhere; nothing edits it by hand, and
|
||||
a stale one self-heals on the next login.
|
||||
|
||||
Cost: **one Keto read + one identity refresh per login** — never per request. JWKS
|
||||
A role can be granted to a user directly or to a **group** the user belongs to; login
|
||||
resolves both (enumerate the defined roles, ask Keto to resolve each membership), so the
|
||||
JWT `roles` match what the admin **Effective access** view shows.
|
||||
|
||||
Cost: **a handful of Keto reads + one identity refresh per login** — never per request. JWKS
|
||||
is cached, so even signature verification hits the network only on key rotation. The
|
||||
app stays stateless; "stay signed in" = re-mint the JWT on a short TTL, the one
|
||||
moment authz is recomputed from Keto.
|
||||
|
||||
Reference in New Issue
Block a user