Make a refused sign-in visible in the log

The reason a sign-in was refused was logged at INFO, in app.py and in
ldap_auth. Nothing in this app configures the root logger, and uvicorn
configures only its own - so an INFO record from wpsuite.* reaches no handler
and is discarded. The message existed and could not be read, in exactly the
situation it was written for: someone cannot sign in and the operator needs to
know whether the credential was wrong, the account is outside the required
group, or the group does not resolve.

Raised to WARNING on the three refusal paths:

  app.py     "sign-in refused for 'x' (not_in_group: not in CN=...)"
  ldap_auth  "bind refused for 'x': 52e (bad password)"
  ldap_auth  "bind succeeded for 'x' but the account is NOT in 'CN=...'"

Left at INFO: provisioning an account, and normalising an address to a
sAMAccountName. Those are narrative, not diagnostic.

Config faults were already ERROR and were always visible, which is why the
503 path could be diagnosed and the 401 path could not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 10:07:17 -05:00
parent 332b74e5de
commit 3f4cd7ac92
2 changed files with 8 additions and 4 deletions

View File

@@ -845,7 +845,11 @@ def login(body: LoginIn, request: Request, response: Response, db: Session = Dep
detail="Sign-in is temporarily unavailable. Contact IT.")
if not result.ok:
log.info("sign-in refused for %r (%s: %s)", sam, result.reason, result.detail)
# WARNING, not INFO: this is the line an operator needs when someone
# cannot sign in, and nothing configures the root logger — under plain
# uvicorn an INFO record from wpsuite.* goes nowhere, so the reason was
# invisible in exactly the situation it exists for.
log.warning("sign-in refused for %r (%s: %s)", sam, result.reason, result.detail)
_record_bind_failure(sam)
if user:
user.failed_attempts = (user.failed_attempts or 0) + 1

View File

@@ -330,7 +330,7 @@ def verify(username: str, password: str, required_group: Optional[str] = None) -
)
if not conn.bind():
detail = _err49(conn.result)
log.info("bind refused for %r: %s", sam, detail)
log.warning("bind refused for %r: %s", sam, detail)
return LdapResult(False, BAD_CREDENTIALS, detail)
# Bound as the user. AD lets an account read its own object, so no
@@ -351,8 +351,8 @@ def verify(username: str, password: str, required_group: Optional[str] = None) -
if group:
try:
if not member_of(conn, sam, group):
log.info("bind succeeded for %r but the account is not in %r",
sam, group)
log.warning("bind succeeded for %r but the account is NOT in %r",
sam, group)
return LdapResult(False, NOT_IN_GROUP, f"not in {group}")
except LookupError:
return LdapResult(False, GROUP_NOT_FOUND, f"group {group!r} not found")