Files
MechaCat02 813bc46640
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 39m53s
CI / Dashboard — check (push) Successful in 10m15s
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>
2026-07-19 20:15:29 +02:00

65 lines
2.1 KiB
Plaintext

// 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
}