docs(examples): add the CMS proof-of-concept dogfooding artifact
A WordPress-inspired CMS whose entire backend is PiCloud Rhai scripts deployed with pic apply, plus a SvelteKit frontend. Built to dogfood the project tool, CLI, and SDK; exercises docs/kv/files/users, docs+queue+cron +pubsub triggers, the transactional-outbox notification chain, a docs before-interceptor, set_if CAS, SSE, per-app CORS, env overlays, and a durable Workflow (validate -> enrich || seo -> publish -> finalize) started with workflow::start and polled with workflow::run_status, visualized live with Svelte Flow. FINDINGS.md / SECURITY.md capture every gap, surprise, and security issue found (each tagged [PiCloud] vs [CMS]); the platform fixes for the actionable ones ship in the preceding commit. Also folds in the read-only `pic` allowlist entries added to .claude/settings.json during the session (fewer-permission-prompts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
64
examples/cms-poc/scripts/auth/auth.rhai
Normal file
64
examples/cms-poc/scripts/auth/auth.rhai
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user