fix(auth): honour an approval granted before sign-up instead of parking the account

Follow-up to the commit fix, from the deep-review. Persisting the account was
only half the story: when the admin had approved the address BEFORE the person
registered, the account landed is_active=False against an invite that is no
longer pending — absent from the Access-requests queue, and approve_invite 409s
on an already-decided invite. The sign-up worked and still could not sign in.

Derive activation from is_allowed() at the moment ownership is attested:
- verify_email_confirm activates a pre-approved account when the emailed link is
  confirmed (only the mailbox owner can follow it), so SEC-10 stays intact;
- _register_account activates only when there is no SMTP to verify with, where
  the admin whitelist is by design the sole gate — the same one add_invite
  already activates on.

Tests: the pre-approved no-SMTP case and the verify-activates case are real
regression tests (both fail without this change); guards cover the unknown-email
and denied->pending branches, verification of an unapproved account, and the
first end-to-end coverage of the /auth/register route and its
allow_registration gate. Release notes rewritten — the previous entry miscounted
its regression tests and advertised an email fix this release has no code for.
This commit is contained in:
2026-09-01 20:56:43 +02:00
parent d954e71a77
commit ea931dca30
3 changed files with 135 additions and 9 deletions
+15
View File
@@ -637,6 +637,15 @@ def _register_account(email: str, password: str) -> None:
db.rollback()
return
upsert_pending_invite(db, email) # admin-approval gate (commits only when it writes a row)
# Honour an approval the admin had ALREADY given for this address. Without this the account
# lands is_active=False against a non-pending invite: it never shows up in the Access-requests
# queue (which lists pending only) and approve_invite 409s on an already-decided invite, so
# the admin has no obvious way to let the person in. Attestation still gates it — with SMTP on
# the address is unverified at this point, so activation waits for the verify link (see
# verify_email_confirm). With no SMTP there is no attestation to wait for and the admin's
# whitelist is by design the only gate, the very one add_invite already activates on.
if not email_ok and is_allowed(db, email):
user.is_active = True
# Persist the account ourselves. upsert_pending_invite commits only when it actually inserts
# (or revives) an invite row, and _issue_token below only runs with SMTP on — so for an email
# the admin had ALREADY approved, on an instance with no SMTP, nothing along this path
@@ -663,6 +672,12 @@ def verify_email_confirm(body: TokenIn, db: Session = Depends(get_db)) -> dict:
status_code=400, detail="This verification link is invalid or has expired."
)
user.email_verified = True
# Ownership is attested now (only the mailbox owner can follow this link), so an approval the
# admin granted BEFORE the person signed up can finally be honoured. Otherwise they sit at
# is_active=False behind a request no admin screen shows — the invite is already decided, so it
# is absent from the pending queue and approve_invite would 409 on it.
if not user.is_active and is_allowed(db, user.email):
user.is_active = True
db.commit()
return {"ok": True}