active? was only checked at the password step of sign-in. A user disabled
afterwards could (a) still complete the 2FA step and mint a valid session, and
(b) keep using any existing session until natural expiry, because per-request
auth only checked session expiry, not user status.
Three enforcement points:
- Mid-flow guard: verify_totp and webauthn_verify re-check active? before
start_new_session_for, clearing the pending session and rejecting if disabled.
- Request-time guard: find_session_by_cookie now uses Session.for_active_user,
so a session whose user is disabled no longer authenticates (authoritative,
catches any disable path including direct DB changes).
- Immediate cleanup: User#revoke_sessions_when_deactivated destroys a user's
sessions when status changes away from active, so access is revoked everywhere
at once rather than on the next request.
Tests cover the mid-flow TOTP rejection, request-time rejection of an existing
session after disable, session destruction on disable, and that unrelated
updates leave sessions intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the implicit "empty allowed_groups means public" rule with
explicit default-deny across both OIDC and ForwardAuth. Adds two boolean
flags on Group — auto_assign (Keycloak-style auto-join on user create)
and admin (members can reach the admin panel) — and drops the
users.admin column entirely. Adds "Users with access" and "Accessible
applications" panels with via-group badges on the application/user show
pages.
BEHAVIOR CHANGE: a ForwardAuth app with no allowed_groups previously
bypassed authentication entirely; it now returns 403 like any other
unauthorized request. The data migration seeds an "everyone" group and
attaches it to all previously group-less apps to preserve behavior on
existing installs. An "admins" group is seeded and backfilled from any
user with the old admin column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>