Repository navigation
Keep profile publication choices to the owner's own sign-in - #778
Merged
TheMorpheus407 merged 2 commits intoOct 6, 2026
Merged
Conversation
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
deleted the
fix/e4-impersonation-session-origin-2026-10-04
branch
October 6, 2026 06:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
sessions.origin(sign_in,impersonation,legacy) andsessions.impersonated_by(the administrator). Password login, registration and OAuth recordsign_in; the admin API recordsimpersonationwith the administrator's id; the CLI recordsimpersonationwithout one. The origin is written once and read from the row on every refresh, so refreshes, page reloads and backend restarts keep it.legacythrough a constant column default. No row is rewritten or updated, so the row triggers onsessionsdo not fire. An insert that names no origin (for example from an older backend after an application-only rollback) also lands onlegacy.impersonated_byreferences the administrator withON DELETE CASCADE, so those sessions end with the administrator's account.PUT /auth/users/me/publication(share, make private again, confirm the notice vianotice_hash/preview_token) answers403 {"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 throughPATCH /auth/users/mewith a changedleaderboard_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 assupport.legacysession 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.The access token format stays unchanged; other services keep verifying the same claims.
Tests
Session::is_owner_sign_in), refresh keeps a stored impersonation origin, the admin API records the administrator, audit attribution for API and CLI impersonation, refusedleaderboard_opt_outchange from an impersonation session.legacy, defaults origin-less inserts tolegacy, 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.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
a6ec5f46a5ba1cd0141bb580016d08522270a1f8treefmt --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, releaseauth_robustness_logsandsecret_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.nix build .#checks(including the VM suites),nix fmt -- --ci,.#scripts.generateand clorinde regeneration byte-identical, valkey/session-valkey/extern/email suites (SMTP through a local stand-in, SMTP authentication not exercised): all passed.platform-acceptance, backend at this head, skills and challenges at develop): 31/31, no skips.tests/publication.pyfails 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