T10.7 - cover the throttle and local is_active; tick what was verified
Two things, both found by checking the wave file before pushing rather than assuming it was current. First: most done-when boxes were still open even where the work had been verified, which would have told a reviewer that almost nothing was checked. Ticked the ones genuinely verified, each with what verified it, and left eight open that are not. One box was not merely unticked but WRONG - it asked that `create-admin` create an admin with no password prompt, and T10.9 removed that command outright; restated as what now has to be true. Second: two of the open boxes were safety-relevant and cheap to close, so ldap_auth_check now covers them (24/24): a disabled local account is refused 403 even though its bind succeeds - local is_active overrides the directory, which is how access to THIS app is revoked without touching the domain account the throttle stops CALLING the directory, not merely refusing. Proved by spending the attempt budget on wrong passwords and then presenting the CORRECT one: a 429 for a credential that would otherwise succeed is only possible if the check runs before the directory is consulted. Also asserts the budget is per-username, so throttling one account does not throttle everyone. That was the last untested safety-critical behaviour on the branch. It is the thing standing between an unauthenticated caller and locking colleagues out of Windows, and until now nothing exercised it. Also recorded a deployment fact at the top of wave-10 where it cannot be missed: LDAP_REQUIRED_GROUP is empty, and empty means no group gate - every account in prime.local may sign in. The live sign-in that confirmed this branch works was made without it, so it proved the bind, the certificate chain and provisioning, but not the group check. That path has still never run against the real directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,55 @@ def main():
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
print("")
|
||||
print("4. local state overrides the directory, and the throttle protects it")
|
||||
db_fd3, db3 = tempfile.mkstemp(suffix=".db"); os.close(db_fd3)
|
||||
import sqlalchemy as _sa0
|
||||
from server.db import Base as _B0
|
||||
from server import models as _m0 # noqa: F401
|
||||
eng0 = _sa0.create_engine("sqlite:///" + db3.replace("\\", "/"))
|
||||
_B0.metadata.create_all(bind=eng0)
|
||||
with eng0.begin() as con:
|
||||
con.execute(_sa0.text(
|
||||
"insert into users (id,username,email,full_name,role,is_active,"
|
||||
"failed_attempts,token_version,project_role,locale,timezone,"
|
||||
"auto_add_projects,auto_add_role,created_at,updated_at) values "
|
||||
"('user_root','root','','Root','project_user',0,0,0,'','','',0,'',"
|
||||
"datetime('now'),datetime('now'))")) # is_active = 0
|
||||
eng0.dispose()
|
||||
port3 = free_port()
|
||||
server3 = start(port3, db3, fake, required_group=GROUP)
|
||||
if server3 is None:
|
||||
print("the third test server would not start.")
|
||||
return 2
|
||||
b3 = f"http://127.0.0.1:{port3}"
|
||||
try:
|
||||
st, _ = post(b3, "/api/auth/login", {"username": "root", "password": PW})
|
||||
chk("a disabled local account is refused even though the bind succeeds",
|
||||
st == 403, st)
|
||||
|
||||
# The throttle. AUTH_MAX_ATTEMPTS is 2, and its whole purpose is that failures
|
||||
# are real domain binds counting against the AD lockout policy — so it has to
|
||||
# stop CALLING the directory, not merely refuse. Proving that: burn the budget
|
||||
# with wrong passwords, then present the CORRECT one. A 429 for a credential
|
||||
# that would otherwise succeed is only possible if the throttle runs before the
|
||||
# directory is consulted.
|
||||
codes = [post(b3, "/api/auth/login",
|
||||
{"username": "outsider", "password": "wrong"})[0] for _ in range(3)]
|
||||
chk("the attempt budget is spent and the next try is throttled",
|
||||
codes[-1] == 429, codes)
|
||||
st, _ = post(b3, "/api/auth/login", {"username": "outsider", "password": PW})
|
||||
chk("...and a CORRECT password is still refused while throttled, proving the "
|
||||
"directory is never reached", st == 429, st)
|
||||
chk("...while another account is unaffected (the budget is per-username)",
|
||||
post(b3, "/api/auth/login", {"username": "root", "password": PW})[0] == 403, "")
|
||||
finally:
|
||||
server3.kill()
|
||||
try:
|
||||
server3.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
print("\n3. an existing admin survives the switch")
|
||||
db_fd2, db2 = tempfile.mkstemp(suffix=".db"); os.close(db_fd2)
|
||||
# Build the schema with a NEW engine bound to this file — see users_in().
|
||||
|
||||
Reference in New Issue
Block a user