// auth — shared module (kind=module). THE access boundary for the CMS. // A public route runs with full app authority (principal:None); every // protected endpoint MUST import this and call require_role/require_user // as its first statement. Imported as: import "auth" as auth; // Extract a bearer token from the Authorization header, or () if absent. fn bearer_token(ctx) { let h = ctx.request.headers["authorization"]; if h == () { return (); } let t = h; if t.starts_with("Bearer ") { t = t.sub_string(7); } else if t.starts_with("bearer ") { t = t.sub_string(7); } t.trim(); if t == "" { return (); } t } // Resolve the current app-user from the bearer token, or () if none/invalid. fn current_user(ctx) { let t = bearer_token(ctx); if t == () { return (); } try { users::verify(t) } catch(e) { () } } // The list of CMS roles, most-privileged first. fn all_roles() { ["admin", "author", "reader"] } // Collect the roles a user actually holds (for /me and UI gating). fn roles_of(user) { let out = []; for r in all_roles() { if users::has_role(user.id, r) { out.push(r); } } out } fn has_any_role(user, roles) { for r in roles { if users::has_role(user.id, r) { return true; } } false } // --- guard pattern ------------------------------------------------------- // We do NOT `throw` an HTTP envelope: an uncaught throw surfaces as a 502 // whose body leaks the app id + script internals (see FINDINGS F-011 / // SECURITY S-01). Instead a guard RETURNS { ok, user } on success or // { ok:false, response } on failure, and the caller does: // let g = auth::guard(ctx, ["admin"]); // if !g.ok { return g.response; } // let user = g.user; fn guard_user(ctx) { let u = current_user(ctx); if u == () { return #{ ok: false, response: #{ statusCode: 401, body: #{ error: "unauthenticated" } } }; } #{ ok: true, user: u } } fn guard(ctx, roles) { let g = guard_user(ctx); if !g.ok { return g; } if !has_any_role(g.user, roles) { return #{ ok: false, response: #{ statusCode: 403, body: #{ error: "forbidden" } } }; } g }