Phase 6: authentication, multi-user, and CSRF protection #69

Manually merged
marvin8 merged 52 commits from feat/multiuser into main 2026-10-06 04:24:28 +00:00
Collaborator

Lands the Phase 6 collecting branch: three efforts making yunjin a proper multi-user application.

Effort 1 — auth, sessions, CSRF. Login/setup/logout, session-derived current_user()/require_current_user() on every route, hand-rolled synchronizer-token CSRF on all state-changing requests, per-user session revocation via users.sessions_valid_after, yunjin set-password CLI, constant-work login verification, safe ?next= redirects.

Effort 2 — data ownership. Global feeds + feed_subscriptions link table, per-user aggregates with watermark + backlog LLM aggregation and per-user no-op skips, unsubscribe with orphan-feed GC invariant, OPML and CLI scoping.

Effort 3 — account management. CLI-only account creation (yunjin create-user, no admin role), self-service password change (current-password gate; current session survives, others revoked), passkey support via webauthn 3.0.1 — username-less login, enrollment and credential management on a new Account card, 12-hour passkey sessions with a server-side replay cap — self-service account deletion with data cleanup (last-user guard in web, CLI unrestricted), yunjin create-user / delete-user CLI, docs (docs/config.md, docs/cli.md, new docs/passkeys.md and docs/account.md). Also closes a setup-gate gap that account deletion made reachable (POST /setup now enforces the recorded per-request gate).

New environment variables: YUNJIN_RP_ID, YUNJIN_ORIGIN (passkeys; disabled unless both set), plus the existing documented session/cookie variables. One dependency added (webauthn~=3.0.1). Docs updated throughout; HTTPS-on-a-stable-domain documented as the passkey prerequisite.

Test suite: 622 passing; full nox green (ty, ruff, complexipy, pysentry, pytest 3.12/3.13/3.14, deptry, zaojun).

Closes #25
Closes #65
Closes #67

Lands the Phase 6 collecting branch: three efforts making yunjin a proper multi-user application. **Effort 1 — auth, sessions, CSRF.** Login/setup/logout, session-derived `current_user()`/`require_current_user()` on every route, hand-rolled synchronizer-token CSRF on all state-changing requests, per-user session revocation via `users.sessions_valid_after`, `yunjin set-password` CLI, constant-work login verification, safe `?next=` redirects. **Effort 2 — data ownership.** Global `feeds` + `feed_subscriptions` link table, per-user aggregates with watermark + backlog LLM aggregation and per-user no-op skips, unsubscribe with orphan-feed GC invariant, OPML and CLI scoping. **Effort 3 — account management.** CLI-only account creation (`yunjin create-user`, no admin role), self-service password change (current-password gate; current session survives, others revoked), passkey support via `webauthn` 3.0.1 — username-less login, enrollment and credential management on a new Account card, 12-hour passkey sessions with a server-side replay cap — self-service account deletion with data cleanup (last-user guard in web, CLI unrestricted), `yunjin create-user` / `delete-user` CLI, docs (`docs/config.md`, `docs/cli.md`, new `docs/passkeys.md` and `docs/account.md`). Also closes a setup-gate gap that account deletion made reachable (`POST /setup` now enforces the recorded per-request gate). New environment variables: `YUNJIN_RP_ID`, `YUNJIN_ORIGIN` (passkeys; disabled unless both set), plus the existing documented session/cookie variables. One dependency added (`webauthn~=3.0.1`). Docs updated throughout; HTTPS-on-a-stable-domain documented as the passkey prerequisite. Test suite: 622 passing; full nox green (ty, ruff, complexipy, pysentry, pytest 3.12/3.13/3.14, deptry, zaojun). Closes #25 Closes #65 Closes #67
🐛 Equalize failed-login work to prevent username enumeration
Some checks failed
/ pr-review (pull_request) Successful in 4s
/ gitleaks (pull_request) Successful in 33s
/ checks (pull_request) Successful in 1m55s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Failing after 1m38s
b7c618a8f5
🔁 Re-trigger CI after PR #63 push non-dispatch
All checks were successful
/ pr-review (pull_request) Successful in 5s
/ gitleaks (pull_request) Successful in 23s
/ checks (pull_request) Successful in 1m56s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m34s
ff1ee472c0
📝 Fix refresh docstring and generics annotation in aggregator
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ pr-review (pull_request) Successful in 1m24s
/ checks (pull_request) Successful in 2m0s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m39s
821d00a07d
✅ Pin unsubscribe effect and document feed-reuse metadata
All checks were successful
/ gitleaks (pull_request) Successful in 38s
/ pr-review (pull_request) Successful in 1m28s
/ checks (pull_request) Successful in 1m48s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m45s
819cb9827f
🔒 Scope aggregate article links to the aggregating user
All checks were successful
/ gitleaks (pull_request) Successful in 22s
/ pr-review (pull_request) Successful in 1m21s
/ checks (pull_request) Successful in 1m58s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m43s
c851e8af5f
✅ Assert the subscription UNIQUE constraint by indexed columns
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ pr-review (pull_request) Successful in 1m21s
/ checks (pull_request) Successful in 2m2s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m59s
5439ead0ca
⏪ Run the nox pytest session with plain pytest
All checks were successful
/ gitleaks (pull_request) Successful in 24s
/ pr-review (pull_request) Successful in 1m35s
/ checks (pull_request) Successful in 18m27s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m42s
bcbf9e80fc
📝 Reword the waitress licence-ignore rationale
All checks were successful
/ gitleaks (pull_request) Successful in 39s
/ pr-review (pull_request) Successful in 1m24s
/ checks (pull_request) Successful in 17m22s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m47s
a49c7862d6
📝 Mark reuse-ignored add-feed options in the CLI table
All checks were successful
/ gitleaks (pull_request) Successful in 38s
/ pr-review (pull_request) Successful in 1m22s
/ checks (pull_request) Successful in 17m14s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m42s
f74f9f2f9f
✅ Cover get_articles_for_feeds and clarify unsubscribe docs
All checks were successful
/ gitleaks (pull_request) Successful in 39s
/ pr-review (pull_request) Successful in 1m30s
/ checks (pull_request) Successful in 17m34s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m47s
7d3b6c2b56
🐛 Close setup-gate bypass and harden registration errors
All checks were successful
/ gitleaks (pull_request) Successful in 27s
/ pr-review (pull_request) Successful in 1m22s
/ checks (pull_request) Successful in 21m11s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m36s
5ee6dc5ea4
POST /setup honored the setup gate only on GET, so a passwordless
leftover account was claimable by an anonymous visitor once any other
user was credentialed. The POST branch now checks setup_required first,
and _ensure_default_user seeds the passwordless default user only when
no users exist at all — deleting user id 1 while other users survive no
longer resurrects a claimable account, while fresh installs and full
teardowns still re-seed default and reopen setup.

Registration verify now maps the UNIQUE-constraint sqlite3.IntegrityError
from a repeated credential id to the generic 400 instead of a 500, and
finish_registration stores transports only when the client JSON carries a
genuine list.
✅ Expect IntegrityError in credential-id uniqueness test
All checks were successful
/ gitleaks (pull_request) Successful in 25s
/ pr-review (pull_request) Successful in 1m16s
/ checks (pull_request) Successful in 21m1s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 3m27s
f05ef6a65b
📝 Say aggregates, not groups, in the passkey deletion docs
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ pr-review (pull_request) Successful in 1m22s
/ checks (pull_request) Successful in 21m38s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m39s
f94e6ca018
📝 Anchor the gitleaks vector regex and round out account docs
Some checks failed
/ gitleaks (pull_request) Successful in 20s
/ pr-review (pull_request) Successful in 1m20s
/ container (pull_request) Has been cancelled
/ publish-container (pull_request) Has been cancelled
/ deploy-docs (pull_request) Has been cancelled
/ checks (pull_request) Has been cancelled
e1a8fb2068
📝 Move account docs to a dedicated page
All checks were successful
/ gitleaks (pull_request) Successful in 26s
/ pr-review (pull_request) Successful in 1m22s
/ checks (pull_request) Successful in 20m58s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m47s
ab3edcdc4b
📝 Qualify passkey gating, operator wording, and seeded default in docs
All checks were successful
/ gitleaks (pull_request) Successful in 17s
/ pr-review (pull_request) Successful in 1m15s
/ checks (pull_request) Successful in 21m16s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m38s
f7fba96d94
✅ Restore session lifetime config via monkeypatch
All checks were successful
/ gitleaks (pull_request) Successful in 20s
/ pr-review (pull_request) Successful in 1m16s
/ checks (pull_request) Successful in 21m13s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m36s
d6318639e0
🔀 Merge branch 'feat/issue-67-account-management' into feat/multiuser
All checks were successful
/ pr-review (pull_request) Successful in 7s
/ gitleaks (pull_request) Successful in 19s
/ checks (pull_request) Successful in 21m5s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m42s
cea51eb532

This PR is too large to review automatically (12670 lines; limit is 5000). Please consider splitting it into smaller PRs.

This PR is too large to review automatically (12670 lines; limit is 5000). Please consider splitting it into smaller PRs.
marvin8 manually merged commit 89f26f7d68 into main 2026-10-06 04:24:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/yunjin!69
No description provided.