NK-WP-0054-T05 phase 3a: founder-run bind-split helper with end-to-end test
prepare-custody, plan, accounts and verify for ld-admin and the svc-* accounts; named rotations for the Authelia bind and LLDAP_LDAP_USER_PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 308701@bnt-lap001 Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
This commit is contained in:
parent
18dae2a0ac
commit
e6f4e28164
6 changed files with 577 additions and 6 deletions
164
sso-mfa/k8s/lldap/test_bind_split.py
Normal file
164
sso-mfa/k8s/lldap/test_bind_split.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""End-to-end test of bind-split.py against a throwaway LLDAP on the pinned image (NK-WP-0054-T05).
|
||||
|
||||
Synthetic only: generated passwords, 127.0.0.1, container and volume removed afterwards. Skipped when
|
||||
docker or the pinned image is unavailable.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
split = load("bind_split", HERE / "bind-split.py")
|
||||
available = (
|
||||
subprocess.run(
|
||||
["docker", "image", "inspect", split.IMAGE], capture_output=True, check=False
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not available, reason="docker or pinned LLDAP image unavailable"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lab(tmp_path):
|
||||
pytest.importorskip("ldap3")
|
||||
rehearsal = load("bind_split_rehearsal", HERE / "bind-split-rehearsal.py")
|
||||
admin_password = secrets.token_hex(20)
|
||||
root = tmp_path / "custody"
|
||||
(root / "lldap").mkdir(parents=True, mode=0o700)
|
||||
root.chmod(0o700)
|
||||
env = root / "lldap/secrets.env"
|
||||
env.write_text(
|
||||
f"LLDAP_JWT_SECRET={secrets.token_hex(32)}\nLLDAP_LDAP_USER_PASS={admin_password}\n"
|
||||
)
|
||||
env.chmod(0o600)
|
||||
directory = rehearsal.Directory(uuid.uuid4().hex[:10])
|
||||
directory.start("admin", admin_password)
|
||||
try:
|
||||
yield root, directory.http, admin_password, directory
|
||||
finally:
|
||||
directory.remove()
|
||||
|
||||
|
||||
def admin_fd(password):
|
||||
read, write = os.pipe()
|
||||
os.write(write, (password + "\n").encode())
|
||||
os.close(write)
|
||||
return read
|
||||
|
||||
|
||||
def test_full_split_on_a_production_shaped_directory(lab, tmp_path):
|
||||
root, endpoint, admin_password, _ = lab
|
||||
assert split.prepare_custody(root)["changed_keys"][0] == "LLDAP_LDAP_USER_PASS"
|
||||
text = (root / "lldap/secrets.env").read_text()
|
||||
assert (
|
||||
admin_password not in text
|
||||
and (root / "lldap/secrets.env").stat().st_mode & 0o077 == 0
|
||||
)
|
||||
for key in ("LLDAP_LDAP_USER_PASS", *split.SERVICE_KEYS):
|
||||
assert re.search(rf"^{key}=[A-Za-z0-9]{{40}}$", text, re.MULTILINE)
|
||||
with pytest.raises(split.Refused):
|
||||
split.prepare_custody(root)
|
||||
|
||||
assert (
|
||||
split.main(
|
||||
[
|
||||
"plan",
|
||||
"--endpoint",
|
||||
endpoint,
|
||||
"--admin-password-fd",
|
||||
str(admin_fd(admin_password)),
|
||||
"--receipt",
|
||||
str(tmp_path / "plan.json"),
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
plan = json.loads((tmp_path / "plan.json").read_text())["result"]["accounts"]
|
||||
assert (
|
||||
all(plan[a]["action"] == "create" for a in split.TARGETS)
|
||||
and plan["admin"]["in_lldap_admin"]
|
||||
)
|
||||
|
||||
directory = split.Directory(endpoint)
|
||||
token = directory.login("admin", admin_password)
|
||||
passwords = split.custody_values(root)
|
||||
dry = split.accounts(directory, token, passwords, apply=False)
|
||||
assert not dry["applied"] and "ld-admin" not in directory.snapshot(token)[0]
|
||||
|
||||
done = split.accounts(directory, token, passwords, apply=True)
|
||||
assert done["applied"] and all(done["signs_in"].values())
|
||||
assert all(done["accounts"][a]["action"] == "ok" for a in split.TARGETS)
|
||||
again = split.accounts(directory, token, passwords, apply=True)
|
||||
assert all(again["accounts"][a]["action"] == "ok" for a in split.TARGETS)
|
||||
|
||||
result = split.verify(directory, token, passwords)
|
||||
assert result["verdict"] == "pass"
|
||||
# The human then changes their own password; the old shared one must stop working.
|
||||
split.set_password(endpoint, token, "admin", secrets.token_hex(20))
|
||||
retired = split.verify(
|
||||
directory,
|
||||
directory.login("ld-admin", passwords["ld-admin"]),
|
||||
passwords,
|
||||
admin_password,
|
||||
)
|
||||
assert retired["old_shared_password_refused"] and retired["verdict"] == "pass"
|
||||
|
||||
receipt = tmp_path / "plan.json"
|
||||
assert admin_password not in receipt.read_text()
|
||||
assert all(value not in receipt.read_text() for value in passwords.values())
|
||||
|
||||
|
||||
def test_existing_account_outside_the_service_domain_is_refused(lab):
|
||||
root, endpoint, admin_password, _ = lab
|
||||
split.prepare_custody(root)
|
||||
directory = split.Directory(endpoint)
|
||||
token = directory.login("admin", admin_password)
|
||||
directory.gql(
|
||||
token,
|
||||
"mutation($u: CreateUserInput!) { createUser(user: $u) { id } }",
|
||||
{"u": {"id": "svc-keycape", "email": "someone@example.test"}},
|
||||
)
|
||||
result = split.accounts(directory, token, split.custody_values(root), apply=True)
|
||||
assert (
|
||||
not result["applied"]
|
||||
and result["accounts"]["svc-keycape"]["action"] == "refuse"
|
||||
)
|
||||
assert "ld-admin" not in directory.snapshot(token)[0]
|
||||
|
||||
|
||||
def test_wrong_admin_password_is_refused_before_any_change(lab, tmp_path):
|
||||
root, endpoint, _, _ = lab
|
||||
split.prepare_custody(root)
|
||||
code = split.main(
|
||||
[
|
||||
"accounts",
|
||||
"--endpoint",
|
||||
endpoint,
|
||||
"--admin-password-fd",
|
||||
str(admin_fd("wrong")),
|
||||
str(root),
|
||||
"--apply",
|
||||
"--receipt",
|
||||
str(tmp_path / "r.json"),
|
||||
]
|
||||
)
|
||||
assert code == 1 and not (tmp_path / "r.json").exists()
|
||||
Loading…
Add table
Add a link
Reference in a new issue