Skip to content

Keep profile publication choices to the owner's own sign-in - #778

Merged
TheMorpheus407 merged 2 commits into
developfrom
fix/e4-impersonation-session-origin-2026-10-04
Oct 6, 2026
Merged

TheMorpheus407 merged 2 commits into
developfrom
fix/e4-impersonation-session-origin-2026-10-04

Conversation

@TheMorpheus407

Copy link
Copy Markdown
Contributor

Why

Finding E4 of the privacy review: an administrator who signs in to an account for troubleshooting (POST /auth/sessions/{user_id}) gets an ordinary session. From it they could share or withdraw the profile, and the receipt would name the owner as its source. Sessions carried no origin, and a token-only marker would be lost on the next refresh. The privacy notice (version 2026-10-r2) promises that we never share a profile on someone's behalf, including during such a sign-in. This change makes that a technical rule.

What changes

  • Durable session origin. sessions.origin (sign_in, impersonation, legacy) and sessions.impersonated_by (the administrator). Password login, registration and OAuth record sign_in; the admin API records impersonation with the administrator's id; the CLI records impersonation without one. The origin is written once and read from the row on every refresh, so refreshes, page reloads and backend restarts keep it.
  • Additive migration, nobody signed out. Existing rows become legacy through a constant column default. No row is rewritten or updated, so the row triggers on sessions do not fire. An insert that names no origin (for example from an older backend after an application-only rollback) also lands on legacy. impersonated_by references the administrator with ON DELETE CASCADE, so those sessions end with the administrator's account.
  • Owner-only decisions. PUT /auth/users/me/publication (share, make private again, confirm the notice via notice_hash/preview_token) answers 403 {"detail":"owner_sign_in_required"} unless the session is the owner's own sign-in. The check runs under the existing account lock, after the in-transaction authority recheck and before any write. The old route through PATCH /auth/users/me with a changed leaderboard_opt_out (it shares or withdraws as well) is refused the same way (403 Owner sign-in required). Reading the choice and the preview stays possible for troubleshooting. Support keeps withdrawing through /auth/admin/users/{user_id}/publication/withdraw, recorded as support.
  • Legacy sessions, defined. A legacy session counts as the owner's sign-in only if it has a device name. Every impersonation path has always created sessions without one (API since the first Rust commit, CLI since its introduction), and no code path changes a session's device name afterwards. Browser sign-ins always carry a User-Agent, so real users keep working without signing in again. Device-less legacy sessions are refused like an impersonation.
  • Audit. Requests made in a session an administrator opened are now recorded in the administrative audit log for that administrator, with the account as target. That includes every refused publication attempt. CLI sessions have no administrator account to attribute and stay out of the log as before.

The access token format stays unchanged; other services keep verifying the same claims.

Tests

  • Unit: origin rules (Session::is_owner_sign_in), refresh keeps a stored impersonation origin, the admin API records the administrator, audit attribution for API and CLI impersonation, refused leaderboard_opt_out change from an impersonation session.
  • PostgreSQL: migration keeps every existing session and refresh token byte for byte, marks them legacy, defaults origin-less inserts to legacy, rejects unknown origins and inconsistent pairs, reverts and reapplies; repository round trip for API/CLI impersonation; impersonation sessions end with the administrator account. The shared demo seed stays usable at historical schema boundaries, and the TOTP migration test is bound to its own migration.
  • HTTP against the real binary with PostgreSQL/Valkey (tests/publication.py): an impersonation session cannot share or withdraw (route and opt-out flag) right after it was issued, after a refresh and after a backend restart; the owner's own session shares and withdraws, also after a refresh; every refused attempt is in the audit log for the administrator; legacy sessions with a device name work, without one they are refused.

Local results at a6ec5f46a5ba1cd0141bb580016d08522270a1f8

  • Same bytes as the commit: treefmt --ci, cargo clippy --all-targets -D warnings (with and without --all-features), scripts/test-unit.py (unit and doc), --suite postgres, scripts/test-auth.py, debug and release builds, release auth_robustness_logs and secret_paths_logs, tests/auth-trace.py, tests/secret-paths-trace.py, tests/backend-safety.py (13 cases), tests/publication.py (15 cases, two new), tests/sensitive-writes.py (96 checks): all passed.
  • At the signed head: nix build .#checks (including the VM suites), nix fmt -- --ci, .#scripts.generate and clorinde regeneration byte-identical, valkey/session-valkey/extern/email suites (SMTP through a local stand-in, SMTP authentication not exercised): all passed.
  • Three-service acceptance (platform-acceptance, backend at this head, skills and challenges at develop): 31/31, no skips.
  • Negative control: with the new check disabled, tests/publication.py fails because the impersonation session's share returns 200.

Hosted GitHub Actions are not available (credits exhausted), so no hosted job is claimed as passing.

Not merged and not deployed: this lands after the release, after an independent review of this exact head.

🤖 Generated with Claude Code

TheMorpheus407 and others added 2 commits October 4, 2026 20:28
Sessions now record who signed in: the owner (password, OAuth or registration), an administrator who signed in to the account (with the administrator's id), the CLI, or "legacy" for sessions from before this change. The origin lives on the session row, so refreshes and restarts keep it.

Sharing, making private again and confirming the publication notice are refused with 403 for every session that is not the owner's own sign-in, on the publication route and through the old leaderboard_opt_out flag. Legacy sessions count as the owner's when they carry a device name, because signing in to someone else's account has never set one. The additive migration signs nobody out.

Requests made in a session an administrator opened are now recorded in the administrative audit log for that administrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Block delegated credential and visibility changes, retain the original operator on descendant sessions, and capture audit actors before requests mutate their sessions. Cover API and native CLI regressions, refresh without a bearer, concurrent revocation, and safe schema rollback.
@TheMorpheus407
TheMorpheus407 added this pull request to the merge queue Oct 6, 2026
Merged via the queue into develop with commit e2c6c1f Oct 6, 2026
11 checks passed
@TheMorpheus407
TheMorpheus407 deleted the fix/e4-impersonation-session-origin-2026-10-04 branch October 6, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant