Keep sessions in step with user account changes - #626
Open
badbread wants to merge 2 commits into
Open
Conversation
A signed-in device could outlive the account behind it. The auth extractor only asked whether a session id had been REVOKED, and `sessions.user_id` is `ON DELETE CASCADE`, so removing a user made the rows disappear rather than be flagged: the removed account's token read as "not revoked" and kept working for the rest of its expiry, which for a remembered mobile login is years. Editing a user changed nothing for tokens already handed out either, and a user's extra camera grants were baked into the token at login, so taking a camera away only took effect at that person's next sign-in. Sessions are now resolved positively. Each session id is looked up against the `sessions` table once and cached in `AppState`, so a missing or revoked row is refused and the warm path still costs no database round trip; an unknown id is resolved on the spot rather than assumed either way, so a token minted a moment ago is never wrongly rejected. The same lookup returns the owning user's camera grants, which the extractor now reads from the user row instead of the token, keeping the existing union with the role's cameras. A login token with no session id is refused: the sessions table predates the first public release, so no released client holds one, and such a token could never be signed out. Removing a user ends their sessions before the row goes. Setting a new password or changing the assigned role ends that user's sessions too, except the one the request came in on when an admin is editing their own account, so changing your own password does not eject you from the console. A save that changes nothing signs nobody out, and changing only the extra-cameras list does not either: that list is now read per request, so it applies immediately anyway. The admin console's user editor gains a "Sign out everywhere" button over the existing admin revoke endpoint, with a confirm that says when it will sign you out of the console as well. Scoped media tokens are untouched: they are a different token type with no session of their own and are resolved before any of this. Adds `services/api/tests/session_lifecycle.rs` covering each case, updates the operator page on users and access, and records the design in docs/DECISIONS.md and docs/COMPONENT-MAP.md. Signed-off-by: badbread <badbread@users.noreply.github.com>
Signed-off-by: badbread <badbread@users.noreply.github.com>
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.
What changed
A signed-in device could outlive the account behind it.
The auth extractor only asked whether a session id had been revoked.
sessions.user_idisON DELETE CASCADE, so removing a user made that user'ssession rows disappear rather than be flagged: the removed account's token read
as "not revoked" and kept working for the rest of its expiry, which for a
remembered mobile login is up to ten years. Editing a user changed nothing for
tokens already handed out, either, and a user's extra camera grants were baked
into the token at login, so taking a camera back only took effect at that
person's next sign-in.
Sessions are now resolved positively:
sessionsonce and cached inAppState, so a missing or revoked row is refused (401) and the warm pathstill costs no database round trip. An unknown id is resolved on the spot
rather than assumed live or dead, so a token minted a moment ago (here or on
another replica) is never wrongly rejected. A database failure surfaces as a
5xx, never a 401, so a transient blip cannot read to a client as "signed
out".
extractor reads those from the user row instead of from
claims.camera_ids,keeping the existing union with the role's cameras exactly as it was. Adding
or withdrawing a grant now applies on that user's next request.
sessionstablepredates the first public release (v0.0.1, 2026-07-06), so no released client
holds such a token, and one could never be signed out.
sessions. The exception is an admin editing their own account: the
session the request came in on is kept, so changing your own password does
not eject you from the console mid-edit. A save that changes nothing signs
nobody out. Changing only the extra-cameras list does not end sessions
either, because that list is read per request and so applies immediately
anyway.
The admin console's user editor gains a Sign out everywhere button over the
existing
DELETE /auth/users/:id/sessionsendpoint, with a confirm that sayswhen it will sign you out of the console too, and a toast reporting how many
sessions ended.
Scoped media tokens are untouched. They are a different token type with no
session of their own, resolved before any of this, and a regression test pins
that.
Why
Token lifetimes here are long by design (the mobile "keep me signed in" option),
so "the account changed" has to reach credentials that were already issued.
Everything above is about the server's answer staying current with the user
table rather than with a snapshot taken at login.
Testing
New suite
services/api/tests/session_lifecycle.rs(real router, realextractor, real Postgres) covering: a removed user's token is 401 on a scoped
route; a password change ends the old session and the new password works; a
role change ends it; an extra-cameras change is reflected on the next request
without re-login, in both the grant and the withdraw direction, with the role's
own cameras unaffected; a no-op save keeps the session; a self-edit keeps the
acting session and ends the admin's other device; a token with no session id is
401; a token whose session row never existed is 401; a scoped media token still
serves segment bytes.
Gate run on the build box against this branch:
cargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings, andcargo test --workspaceagainst a throwaway Postgres, all green.
node --checkon the extractedadmin.htmlscript block passes.Operator-visible
db/migrations/0033_sessions.sqlwere brought up to date; the SQL isuntouched).
or changes their role, and lose access immediately when their account is
removed. Every client already handles a 401 by returning to its sign-in
screen, so no client change was needed.
docs-site/docs/admin-console/users-and-access.mdgains a short"Signing devices out" section. Design recorded in
docs/DECISIONS.md(2026-09-07) and a new row in
docs/COMPONENT-MAP.mdsection G.Gate output:
GATE_OK fix/session-lifecycle(exit 0) on commit 5b3f6a6.