"""Synthetic tests for the KeyCape LLDAP bind switch (NK-WP-0054-T05); no cluster, no real values.""" import base64 import copy import importlib.util import json import types from pathlib import Path import pytest import yaml ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location( "bind_rollout", ROOT / "sso-mfa/k8s/keycape/directory-bind-rollout.py" ) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) SERVICE_PASSWORD = "S" * 20 + "0123456789abcdefghij" HUMAN_PASSWORD = "synthetic-human-password" CONFIG = """# synthetic KeyCape config issuer: https://kc.example.test signingKey: | -----BEGIN SYNTHETIC----- abc lldap: url: ldap://lldap.sso.svc.cluster.local:3890 bindDN: uid=admin,ou=people,dc=netkingdom,dc=local bindPW: old-shared-password baseDN: dc=netkingdom,dc=local authelia: issuer: https://auth.coulomb.social clientSecret: synthetic-client clients: - clientId: one """ def secret(config=CONFIG, version="41"): return { "metadata": { "uid": "11111111-2222-3333-4444-555555555555", "resourceVersion": version, }, "data": { "config.yaml": base64.b64encode(config.encode()).decode(), "other": "b3RoZXI=", }, } class Cluster: def __init__(self, obj): self.obj, self.calls = obj, [] def __call__(self, args, payload=None): self.calls.append((args, payload)) if args[:2] == ["get", "namespace"]: return {"metadata": {"uid": m.lane.CLUSTER_UID}} if "get" in args: return copy.deepcopy(self.obj) assert payload[0]["value"] == self.obj["metadata"]["uid"] assert payload[1]["value"] == self.obj["metadata"]["resourceVersion"] result = copy.deepcopy(self.obj) result["data"]["config.yaml"] = payload[2]["value"] if "--dry-run=server" not in args: self.obj = dict( result, metadata=dict(result["metadata"], resourceVersion="42") ) return result def args(mode="inspect", operation="switch", attended=False, version="41"): return types.SimpleNamespace( mode=mode, operation=operation, custody_root="unused", attended=attended, expected_uid="11111111-2222-3333-4444-555555555555", expected_resource_version=version, ) @pytest.fixture(autouse=True) def custody(monkeypatch): monkeypatch.setattr(m, "service_password", lambda root: SERVICE_PASSWORD) def config_of(obj): return base64.b64decode(obj["data"]["config.yaml"]).decode() def test_switch_changes_exactly_the_two_bind_scalars(): cluster = Cluster(secret()) receipt = m.run(args("apply", attended=True), cluster) assert ( receipt["changed"] and receipt["prior_bind"] == "human" and receipt["restart_required"] ) after = config_of(cluster.obj) expected = yaml.safe_load(CONFIG) expected["lldap"].update(bindDN=m.SERVICE_DN, bindPW=SERVICE_PASSWORD) assert yaml.safe_load(after) == expected assert after.replace(json.dumps(m.SERVICE_DN), "X").replace( json.dumps(SERVICE_PASSWORD), "Y" ) == CONFIG.replace("uid=admin,ou=people,dc=netkingdom,dc=local", "X").replace( "old-shared-password", "Y" ) assert cluster.obj["data"]["other"] == "b3RoZXI=" assert SERVICE_PASSWORD not in json.dumps( receipt ) and "old-shared-password" not in json.dumps(receipt) def test_inspect_and_dry_run_write_nothing(): cluster = Cluster(secret()) assert m.run(args("inspect"), cluster)["change_needed"] m.run(args("dry-run"), cluster) assert config_of(cluster.obj) == CONFIG def test_apply_needs_attended_and_the_observed_revision(): with pytest.raises(m.Refused, match="attended"): m.run(args("apply"), Cluster(secret())) with pytest.raises(m.Refused, match="observed_revision_changed"): m.run(args("apply", attended=True, version="40"), Cluster(secret())) def test_second_switch_is_a_noop_and_revert_restores_the_human_bind(): cluster = Cluster(secret()) m.run(args("apply", attended=True), cluster) again = m.run(args("apply", attended=True, version="42"), cluster) assert not again["change_needed"] and again["prior_bind"] == "service" reverted = m.run( args("apply", "revert", attended=True, version="42"), cluster, prompt=lambda _: HUMAN_PASSWORD, ) assert reverted["changed"] assert yaml.safe_load(config_of(cluster.obj))["lldap"] == dict( yaml.safe_load(CONFIG)["lldap"], bindPW=HUMAN_PASSWORD ) @pytest.mark.parametrize( "change", [ lambda c: c.replace("uid=admin,", "uid=someone,"), lambda c: c.replace(" bindPW: old-shared-password\n", ""), lambda c: c.replace("lldap:\n", "lldap: &a\n"), ], ) def test_unexpected_shapes_are_refused(change): with pytest.raises((m.Refused, m.pin.IssuerPinError)): m.run(args("inspect"), Cluster(secret(change(CONFIG)))) def test_main_contains_failures_without_values(capsys): def broken(args, payload=None): raise RuntimeError("Secret data: old-shared-password") assert m.main(["inspect", "--custody-root", "x"], kubectl=broken) == 1 out = capsys.readouterr().out assert "contained_operation_failed" in out and "old-shared-password" not in out