From 3f4cd7ac923139815a0cacff58a983ddb8afe5b7 Mon Sep 17 00:00:00 2001 From: Cody Schaefer Date: Mon, 24 Aug 2026 10:07:17 -0500 Subject: [PATCH] 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) --- server/app.py | 6 +++++- server/ldap_auth.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/app.py b/server/app.py index ddeef59..5d530f3 100644 --- a/server/app.py +++ b/server/app.py @@ -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 diff --git a/server/ldap_auth.py b/server/ldap_auth.py index ce1f89c..11c14c0 100644 --- a/server/ldap_auth.py +++ b/server/ldap_auth.py @@ -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")