T10.7 - close four more done-when boxes offline; token_version was mis-specified
I had said all six remaining boxes needed the live environment. Four did not,
and saying so was lazy scoping. ldap_auth_check now covers them (32/32):
a just-provisioned account appears in GET /api/auth/users, the request the
Admin console makes - without which nobody could grant a JIT account access
granting admin through POST /api/auth/users/{id}/role, the request users.js
sends, with the role verified to have actually changed. D13 criterion 4 was
until now only asserted for role PRESERVATION, never for role GRANTING.
no AD error-49 sub-code appears in any response body, while _err49 does parse
one out of a real AD message - the log gets the detail, the caller does not
token_version invalidates a cookie already issued, and leaves other sessions
alone
That last box was wrong as written. It asked to exercise token_version "by a
role change", and nothing in app.py bumps it on a role change - or on a
deactivation. Neither needs to: get_current_user re-reads the account from the
database every request, so both take effect on the next request regardless.
T10.3's note that "role changes and deactivation should bump it" described an
intention rather than the code, and I repeated it without checking.
What token_version actually is now: a mechanism whose only trigger is the bump
manage_users makes on disable, which is belt-and-braces since is_active already
refuses the request. It works, and it is tested - but nothing much triggers it.
Logged as BL-030 rather than resolved here, because whether to wire it to
something (a "sign out everywhere" control is the usual shape) or remove it is
a session-handling design question, not an auth-wave bug.
Two boxes remain open, and both genuinely need your environment: member_of
against a real NESTED group, and the in-container openssl certificate check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ from cdp import free_port # noqa: E40
|
||||
_PASS, _FAIL = [], []
|
||||
PW = "CorrectHorseBattery9"
|
||||
GROUP = "WP-Suite-Users"
|
||||
SECRET = "ldap-auth-check-not-for-production"
|
||||
os.environ.setdefault("AUTH_SECRET_KEY", SECRET)
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
@@ -61,10 +63,31 @@ def post(base, path, payload):
|
||||
return e.code, {}
|
||||
|
||||
|
||||
def get(base, path, token):
|
||||
req = urllib.request.Request(base + path, headers={"Cookie": f"wp_session={token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return r.status, json.loads(r.read().decode() or "{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {}
|
||||
|
||||
|
||||
def post_as(base, path, token, payload):
|
||||
req = urllib.request.Request(base + path, method="POST",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json",
|
||||
"Cookie": f"wp_session={token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return r.status, json.loads(r.read().decode() or "{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {}
|
||||
|
||||
|
||||
def start(port, db_path, fake, required_group=""):
|
||||
env = dict(os.environ)
|
||||
env["DATABASE_URL"] = "sqlite:///" + db_path.replace("\\", "/")
|
||||
env["AUTH_SECRET_KEY"] = "ldap-auth-check-not-for-production"
|
||||
env["AUTH_SECRET_KEY"] = SECRET
|
||||
env["WP_LDAP_FAKE_DIRECTORY"] = json.dumps(fake)
|
||||
if required_group:
|
||||
env["LDAP_REQUIRED_GROUP"] = required_group
|
||||
@@ -301,6 +324,84 @@ def main():
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
print("")
|
||||
print("5. the console's own routes, and what token_version actually does")
|
||||
db_fd4, db4 = tempfile.mkstemp(suffix=".db"); os.close(db_fd4)
|
||||
import sqlalchemy as _sa1
|
||||
from server.db import Base as _B1
|
||||
from server import models as _m1, auth as _auth # noqa: F401
|
||||
url4 = "sqlite:///" + db4.replace("\\", "/")
|
||||
eng1 = _sa1.create_engine(url4)
|
||||
_B1.metadata.create_all(bind=eng1)
|
||||
with eng1.begin() as con:
|
||||
for uid, un, role in [("user_boss", "root", "admin"),
|
||||
("user_pat", "pat", "project_user")]:
|
||||
con.execute(_sa1.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 "
|
||||
f"('{uid}','{un}','','{un}','{role}',1,0,0,'','','',0,'',"
|
||||
"datetime('now'),datetime('now'))"))
|
||||
|
||||
class _U:
|
||||
pass
|
||||
def _tok(uid, un, role):
|
||||
u = _U(); u.id = uid; u.username = un; u.role = role; u.token_version = 0
|
||||
return _auth.create_token(u)
|
||||
boss_tok = _tok("user_boss", "root", "admin")
|
||||
pat_tok = _tok("user_pat", "pat", "project_user")
|
||||
|
||||
port4 = free_port()
|
||||
server4 = start(port4, db4, fake, required_group="")
|
||||
if server4 is None:
|
||||
print("the fourth test server would not start.")
|
||||
return 2
|
||||
b4 = f"http://127.0.0.1:{port4}"
|
||||
try:
|
||||
st, body = get(b4, "/api/auth/users", boss_tok)
|
||||
chk("an admin can list accounts", st == 200, st)
|
||||
|
||||
post(b4, "/api/auth/login", {"username": "outsider", "password": PW})
|
||||
st, body = get(b4, "/api/auth/users", boss_tok)
|
||||
rows = body if isinstance(body, list) else (body.get("users") or body.get("items") or [])
|
||||
names = [u.get("username") for u in rows if isinstance(u, dict)]
|
||||
chk("a just-provisioned account appears in the Admin console list",
|
||||
"outsider" in names, names)
|
||||
|
||||
# Exactly the request users.html sends — D13 criterion 4.
|
||||
st, _ = post_as(b4, "/api/auth/users/user_pat/role", boss_tok, {"role": "admin"})
|
||||
chk("granting admin through the console route succeeds", st == 200, st)
|
||||
chk("...and the role really changed", users_in(db4).get("pat") == "admin", users_in(db4))
|
||||
|
||||
# token_version. NOTHING in app.py bumps it any more: is_active and role are
|
||||
# re-read from the database every request, so both take effect at once without
|
||||
# it. What it still does is invalidate an ALREADY-ISSUED cookie, which is what
|
||||
# manage_users does on disable. Bump it directly and prove the effect.
|
||||
eng2 = _sa1.create_engine(url4)
|
||||
with eng2.begin() as con:
|
||||
con.execute(_sa1.text("update users set token_version = 1 where id='user_pat'"))
|
||||
eng2.dispose()
|
||||
st, _ = get(b4, "/api/auth/me", pat_tok)
|
||||
chk("bumping token_version invalidates a cookie already issued", st == 401, st)
|
||||
st, _ = get(b4, "/api/auth/me", boss_tok)
|
||||
chk("...and leaves every other session alone", st == 200, st)
|
||||
|
||||
st, body = post(b4, "/api/auth/login", {"username": "root", "password": "wrong"})
|
||||
blob = json.dumps(body).lower()
|
||||
chk("no AD sub-code leaks into a response body",
|
||||
not any(c in blob for c in ("52e", "525", "532", "533", "775", "data ")), body)
|
||||
from server import ldap_auth as _la
|
||||
chk("...though _err49 does parse one when AD sends it",
|
||||
"52e" in _la._err49({"message": "80090308: LdapErr: DSID-0C0903A9, comment: "
|
||||
"AcceptSecurityContext error, data 52e, v4563"}))
|
||||
finally:
|
||||
eng1.dispose()
|
||||
server4.kill()
|
||||
try:
|
||||
server4.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
print("\n" + "-" * 54)
|
||||
print(f"{len(_PASS)}/{len(_PASS) + len(_FAIL)} checks passed.")
|
||||
for f in _FAIL:
|
||||
|
||||
Reference in New Issue
Block a user