T10.9 fix - the CLI's auth failure said nothing useful
"Authentication failed." and no more. I had carried the generic-message reasoning across from /api/auth/login, where one indistinguishable failure is correct because an unauthenticated caller must not be able to enumerate accounts. That reasoning does not transfer to a local CLI: the operator IS the account holder, there is nobody to leak to, and withholding AD's own sub-code just makes the failure undiagnosable. It cost a round trip to find that out. Now prints the reason, the AD error-49 sub-code, the exact bind string attempted, the server, and the password length - enough to tell apart a wrong password (52e), a locked account (775), an expired one (532), and the case that looks like a bad password but is not: 525, no such user, which means the BIND NAME is wrong rather than the credential. That last one matters here. The bind is <sAMAccountName>@LDAP_DOMAIN, and LDAP_DOMAIN defaults to the AD DNS name (prime.local). If an estate's UPN suffix is the mail domain instead, that string is not a valid bind name and AD answers 525 - which reads as "bad password" to anyone not looking at sub-codes. The hint says so, and says to set LDAP_DOMAIN to the UPN suffix. The password is never echoed or logged; only its length, which distinguishes "getpass read nothing" from "getpass read the wrong thing". /api/auth/login is untouched. Its generic 401 is still correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,9 +84,27 @@ def authenticate_operator() -> str:
|
||||
# not be able to lock an operator out of the tool that fixes the login group.
|
||||
result = ldap_auth.verify(who, pw, required_group="")
|
||||
if not result.ok:
|
||||
if result.is_config_problem:
|
||||
sys.exit(f"Could not reach the domain ({result.reason}): {result.detail}")
|
||||
sys.exit("Authentication failed.")
|
||||
# SAY WHY. The /api/auth/login endpoint deliberately returns one generic
|
||||
# message so an unauthenticated caller cannot enumerate accounts; that
|
||||
# reasoning does NOT transfer here. This is a local tool, the operator is
|
||||
# the account holder, and there is nobody to leak to — so withholding the
|
||||
# AD sub-code only makes a failure undiagnosable. An earlier version of
|
||||
# this function printed "Authentication failed." and nothing else.
|
||||
print(f"Authentication failed: {result.reason} — {result.detail}", file=sys.stderr)
|
||||
print(f" bind attempted as : {ldap_auth.normalize_username(who)}@{ldap_auth.DOMAIN}",
|
||||
file=sys.stderr)
|
||||
print(f" server : ldaps://{ldap_auth.HOST}:{ldap_auth.PORT}", file=sys.stderr)
|
||||
print(f" password length : {len(pw)} characters", file=sys.stderr)
|
||||
if result.reason == ldap_auth.BAD_CREDENTIALS:
|
||||
print(" The sub-code above is AD's own reason: 52e = wrong password, "
|
||||
"775 = account locked out, 532 = password expired, "
|
||||
"533 = account disabled, 525 = no such user.", file=sys.stderr)
|
||||
print(" A 525 with a password you know is correct means the BIND NAME is "
|
||||
"wrong, not the password. This binds as <sAMAccountName>@LDAP_DOMAIN, "
|
||||
"which only works where that matches your real UPN suffix — set "
|
||||
"LDAP_DOMAIN to the UPN suffix if yours differs from the AD DNS name.",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"Authenticated as {result.sam}.")
|
||||
return result.sam
|
||||
|
||||
|
||||
Reference in New Issue
Block a user