net-kingdom/tests/test_privacyidea_verification.py
tegwick 4e07d60ff1
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Validate cadence contract and require functional MFA verification
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06ea3-7939-7b63-8125-699f8b50bedd
2026-09-05 01:28:05 +02:00

218 lines
7.1 KiB
Python

"""Functional failure matrix through the real HTTP transport, no live credentials."""
import copy
import importlib
import json
import subprocess
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
ROOT = Path(__file__).resolve().parents[1]
HELPERS = ROOT / "sso-mfa/k8s/privacyidea"
sys.path.insert(0, str(HELPERS))
pi_api = importlib.import_module("pi_api")
verify_mfa = importlib.import_module("verify_mfa")
def result(value, **extra):
return {"result": {"status": True, "value": value}, **extra}
@pytest.fixture
def target():
state = {
"/auth": result({"token": "test-token"}),
"/realm/": result({"coulomb": {"resolver": [{"name": "lldap-coulomb"}]}}),
"/resolver/lldap-coulomb": result(
{
"lldap-coulomb": {
"type": "ldapresolver",
"data": {
"TIMEOUT": "5",
"CACHE_TIMEOUT": "120",
"SIZELIMIT": "500",
},
}
}
),
"/user/": result([{"username": "test-user", "resolver": "lldap-coulomb"}]),
"/validate/check": result(True, detail={"serial": "TOTP-TEST", "type": "totp"}),
"requests": [],
}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
self.respond()
def do_POST(self):
self.respond()
def respond(self):
parsed = urlsplit(self.path)
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
state["requests"].append(
(self.command, self.path, dict(self.headers), body)
)
# Reproduce the original server rejection at the request boundary.
if self.command == "GET" and self.headers.get("Content-Type"):
self.send_error(400)
return
if parsed.path == "/user/":
assert parse_qs(parsed.query) == {
"realm": ["coulomb"],
"username": ["test-user"],
}
response = state.get(parsed.path)
if response is None:
self.send_error(403)
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(response).encode())
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}", state
finally:
server.shutdown()
server.server_close()
thread.join()
def run(url):
return verify_mfa.verify(
url,
"test-user",
"coulomb",
"lldap-coulomb",
"test-password",
lambda: "test-otp",
)
@pytest.mark.parametrize("field", ["TIMEOUT", "CACHE_TIMEOUT", "SIZELIMIT"])
def test_clear_and_restore_tuning(target, field):
url, state = target
data = state["/resolver/lldap-coulomb"]["result"]["value"]["lldap-coulomb"]["data"]
original = data.pop(field)
assert run(url) == {"result": "FAIL", "phase": "resolver-tuning"}
assert all(urlsplit(r[1]).path != "/validate/check" for r in state["requests"])
data[field] = original
assert run(url)["result"] == "PASS"
@pytest.mark.parametrize(
"route,replacement,phase",
[
("/auth", None, "authentication"),
("/auth", result({}), "authentication"),
("/realm/", result({"coulomb": {"resolver": []}}), "realm-binding"),
("/user/", result([]), "known-user-lookup"),
(
"/user/",
result([{"username": "other", "resolver": "lldap-coulomb"}]),
"known-user-lookup",
),
(
"/user/",
result([{"username": "test-user", "resolver": "other"}]),
"known-user-lookup",
),
("/user/", {"result": {"status": False, "value": []}}, "known-user-lookup"),
("/validate/check", result(False), "mfa-validation"),
("/validate/check", result(True), "mfa-validation"),
(
"/validate/check",
result(True, detail={"serial": "PASSWORD", "type": "spass"}),
"mfa-validation",
),
("/validate/check", result(True, detail=None), "mfa-validation"),
],
)
def test_failures_never_become_success(target, route, replacement, phase):
url, state = target
state[route] = copy.deepcopy(replacement)
assert run(url) == {"result": "FAIL", "phase": phase}
def test_transport_and_shell_adapter_share_bodyless_get_behavior(target):
url, state = target
assert run(url)["result"] == "PASS"
proc = subprocess.run(
[sys.executable, str(HELPERS / "pi_api.py"), "GET", url + "/realm/"],
input="test-token\n",
text=True,
capture_output=True,
check=False,
)
assert proc.returncode == 0
assert json.loads(proc.stdout)["result"]["status"] is True
for method, path, headers, body in state["requests"]:
if method == "GET":
assert "Content-Type" not in headers
assert not body
else:
assert headers["Content-Type"] == "application/json"
assert isinstance(json.loads(body), dict)
def test_shell_entrypoint_requires_attendance_and_does_not_read_bundle():
proc = subprocess.run(
["bash", str(ROOT / "sso-mfa/k8s/verify-t06.sh"), "--user", "test-user"],
stdin=subprocess.DEVNULL,
capture_output=True,
check=False,
text=True,
)
assert proc.returncode == 2
assert "attended terminal" in proc.stderr
assert not proc.stdout
@pytest.mark.parametrize("policy_ok", [True, False])
def test_bootstrap_uses_shared_transport_and_reports_full_result(
target, tmp_path, policy_ok
):
url, state = target
for route in [
"/realm/coulomb",
"/defaultrealm/coulomb",
"/policy/totp-self-enrollment",
"/policy/coulomb-friendly-token-labels",
"/policy/mfa-passthru-phase1",
]:
state[route] = result(True)
if not policy_ok:
state["/policy/totp-self-enrollment"] = None
for folder, content in [
("privacyidea", "PI_ADMIN_PASSWORD=test-password\n"),
("lldap", "LLDAP_LDAP_USER_PASS=test-bind-password\n"),
]:
directory = tmp_path / folder
directory.mkdir()
(directory / "secrets.env").write_text(content)
proc = subprocess.run(
["bash", str(HELPERS / "bootstrap-realm.sh"), str(tmp_path), url],
capture_output=True,
text=True,
check=False,
)
assert proc.returncode == (0 if policy_ok else 1)
assert "Realm bootstrap: PASS=" in proc.stdout
assert "FAIL=0" in proc.stdout if policy_ok else "FAIL=1" in proc.stdout
for secret in ["test-token", "test-password", "test-bind-password"]:
assert secret not in proc.stdout + proc.stderr
assert any(
method == "GET" and path == "/resolver/lldap-coulomb"
for method, path, _, _ in state["requests"]
)