fix(auth): stop discarding a legitimate password on admin approval (E9.5 S1)
SEC-10 revoked any password set while an account's email was unverified, on both the Google-adoption and the admin-approval path. On the approval path that was redundant: password_login refuses an unverified account before it ever reads is_active, so approval hands a pre-emptor's password nothing it could use. All the revoke achieved there was destroying the password a legitimate registrant had just chosen -- and since the admin normally decides before the user reads their mail, 'your password was cleared for security' was the DEFAULT experience, not an edge case. The guard stays on the Google-adoption path, where the real owner does attest ownership. password_reset_required and the ok_reset banner stay: accounts stripped on prod before this change have no other way back in. Two existing tests pinned the old rule, so they are rewritten rather than deleted, and the SEC-10 recovery pack now builds the revoked state through the surviving producer.
This commit is contained in:
@@ -27,7 +27,6 @@ from app.models import (
|
||||
Invite,
|
||||
User,
|
||||
)
|
||||
from app.routes.auth import revoke_preset_password_if_unverified
|
||||
from app.utils import iso, valid_email
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
@@ -88,13 +87,18 @@ def list_invites(
|
||||
|
||||
def _activate_user_by_email(db: Session, email: str) -> None:
|
||||
"""Approving access activates a matching (e.g. pending password-registration) account so
|
||||
it can sign in. No-op if no such user exists yet (a Google user is created at first login)."""
|
||||
it can sign in. No-op if no such user exists yet (a Google user is created at first login).
|
||||
|
||||
E9.5 S1: this path deliberately does NOT revoke a preset password. Approval is not an ownership
|
||||
attestation, and it does not need to be one — `password_login` refuses an unverified account
|
||||
BEFORE it looks at `is_active`, so a pre-emptor who set a password on someone else's address
|
||||
still cannot sign in with it, approved or not. Revoking here only ever destroyed the password a
|
||||
legitimate registrant had just chosen, and made "your password was cleared for security" the
|
||||
DEFAULT experience, because approve-before-verify is the normal ordering. The revoke stays on
|
||||
the Google-adoption path (auth.revoke_preset_password_if_unverified), where the real owner does
|
||||
attest ownership and the preset password must not survive."""
|
||||
user = db.execute(select(User).where(User.email == email.lower())).scalar_one_or_none()
|
||||
if user is not None and not user.is_active:
|
||||
# SEC-10: same guard as the Google-adoption path — approving an account whose email was never
|
||||
# verified must not hand a working login to a password set on it by whoever registered the
|
||||
# address (a pre-emption). Shared single-source helper so the two paths can't drift.
|
||||
revoke_preset_password_if_unverified(user)
|
||||
user.is_active = True
|
||||
db.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user