Phase 16: Petal authenticates for itself
Petal is now an OIDC client in its own right rather than trusting a header from the proxy. The Phase-0 Resolver seam was the only integration point: main.go picks the session store when Authentik is configured and the static local user otherwise, and no handler or query moved for either. internal/auth gains three pieces. session.go issues an opaque cookie token and stores only its SHA-256, so a database copy yields nothing usable; the 30-day expiry slides on every request, throttled to one write an hour, and logout deletes the row rather than just the cookie. oidc.go runs the authorization-code flow with state, nonce and PKCE, and discovers the provider lazily and on retry — an Authentik outage should block new logins without stopping Petal booting or invalidating live sessions. users.go provisions accounts from the token's claims and gates them on an allowlist that matches emails as well as subject ids, since a subject is an opaque uuid that doesn't exist until someone has already logged in once. Migration 0010 lands sessions, images and users.pair_lang together. The images table closes the capability-URL hole the Phase-0 audit flagged: a hash was previously enough to fetch anyone's picture. Rows are keyed (name, user_id) so one file can have several owners and deduplication survives; a stranger gets 404 rather than 403, the cache header drops to private, and files already on disk are claimed at startup or every image already pasted into a document would 404. On the frontend a single 401 interceptor feeds a warm bilingual sign-in overlay, drawn over a still-visible editor because nothing has been taken away. Behind it is the part that matters: a save that comes back 401 stashes its body to localStorage before anything else and stops the auto-save loop, and reopening that document after signing in merges the draft back and saves it. An expired session must not cost writing. Writing the round-trip test against a stub identity provider turned up a real bug: the one-shot state/nonce/PKCE cookies were cleared in a defer, which runs after the redirect has written the response header, so the clearing Set-Cookie was silently dropped and they lingered for their full ten minutes. Also swaps the emoji favicon for a drawn sakura, which renders as Petal's own rose palette everywhere instead of whatever each platform's font decides, and doubles as the app tile in Authentik. Migration 0010 verified against a VACUUM INTO copy of the live millenia database: counts intact, FTS still matching, the one existing image claimed. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
@@ -390,6 +390,52 @@ CREATE INDEX idx_suggestions_doc_id ON suggestions(doc_id);
|
||||
ALTER TABLE documents ADD COLUMN preserve_history INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE document_versions ADD COLUMN content_hash TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE document_versions ADD COLUMN prev_hash TEXT NOT NULL DEFAULT '';
|
||||
`,
|
||||
},
|
||||
{
|
||||
// Real accounts. Three separate things land together because they are
|
||||
// one change: Petal can now tell users apart.
|
||||
//
|
||||
// `sessions` backs server-side login state. The cookie carries an opaque
|
||||
// random token and this table stores only its SHA-256 — a leaked database
|
||||
// copy therefore yields no usable session, the same reason passwords are
|
||||
// hashed. Server-side rows (rather than a signed stateless cookie) are
|
||||
// what make logout and revocation actually revoke.
|
||||
//
|
||||
// `images` gives the content-addressed image store an owner. Until now it
|
||||
// was a flat directory with no database row at all: any caller holding a
|
||||
// hash could fetch anyone's image, which is capability-URL security, not
|
||||
// access control. The primary key is (name, user_id), so the same picture
|
||||
// uploaded by two people is still stored once on disk and simply has two
|
||||
// rows — deduplication survives; the file is deleted only with its last
|
||||
// row. Rows for images already on disk are backfilled at startup by the
|
||||
// images package, which is the only code that knows the storage path.
|
||||
//
|
||||
// `users.pair_lang` is the writer's language pair (English + X). It is
|
||||
// unused until the langpack work, but it belongs to provisioning and
|
||||
// costs nothing to add while the users table is already being touched.
|
||||
name: "0010_sessions_images_and_pair_lang",
|
||||
stmt: `
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_agent TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||||
|
||||
CREATE TABLE images (
|
||||
name TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
content_type TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (name, user_id)
|
||||
);
|
||||
CREATE INDEX idx_images_user_id ON images(user_id);
|
||||
|
||||
ALTER TABLE users ADD COLUMN pair_lang TEXT NOT NULL DEFAULT 'zh';
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user