import hashlib import json import os import stat import subprocess import yaml from unittest.mock import MagicMock, patch import pytest from ops_mason.executor import ( AppRoleKVSpec, BuildError, BuildRefused, KubernetesKVSpec, _policy_hcl, build_spec_document, build_spec_digest, build_approle_kv_lane, build_kubernetes_kv_lane, ) def test_policy_hcl_uses_kv_v2_data_and_metadata_paths() -> None: hcl = _policy_hcl("reins/rein-openweights/openrouter", ("read",)) assert 'path "reins/data/rein-openweights/openrouter" {' in hcl assert 'path "reins/metadata/rein-openweights/openrouter" {' in hcl assert 'path "reins/rein-openweights/openrouter" {' not in hcl assert 'capabilities = ["read"]' in hcl from ops_mason.plan import ConstructionPlan POLICY = 'path "reins/data/test/openrouter" { capabilities = ["read"] }\n' POLICY_HASH = hashlib.sha256(POLICY.encode()).hexdigest() def _plan(tmp_path, *, status="approved", approved_by="bernd", approved_at="2026-07-27", spec=None): document = build_spec_document(spec or _spec(tmp_path)) fields = dict(id="test-lane", status=status, approved_by=approved_by, approved_at=approved_at, credential_type=document["engine"], build_spec=document, approved_spec_sha256=build_spec_digest(document)) p = tmp_path / "plan.md" p.write_text("---\n" + yaml.safe_dump(fields) + "---\n# Plan\n") return ConstructionPlan.load(p) def _spec(tmp_path) -> AppRoleKVSpec: return AppRoleKVSpec( policy_name="workload-kv-read-test-lane", kv_path="reins/test/openrouter", approle_name="test-lane", token_num_uses=8, delivery_dir=tmp_path / "delivery", audit_log_path=tmp_path / "audit.jsonl", ) def test_token_num_uses_has_no_silently_unbounded_default() -> None: import inspect sig = inspect.signature(AppRoleKVSpec) assert sig.parameters["token_num_uses"].default is inspect.Parameter.empty def test_refuses_when_not_approved(tmp_path) -> None: plan = _plan(tmp_path, status="draft", approved_by=None, approved_at=None) with pytest.raises(BuildRefused, match="not approved"): build_approle_kv_lane(plan, _spec(tmp_path)) def test_refuses_when_approved_status_but_missing_approver(tmp_path) -> None: plan = _plan(tmp_path, status="approved", approved_by=None, approved_at="2026-07-27") with pytest.raises(BuildRefused, match="not approved"): build_approle_kv_lane(plan, _spec(tmp_path)) def test_refusal_never_calls_bao(tmp_path) -> None: plan = _plan(tmp_path, status="reviewed", approved_by=None, approved_at=None) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_approle_kv_lane(plan, _spec(tmp_path)) run.assert_not_called() def test_build_writes_policy_approle_and_delivers_credentials(tmp_path) -> None: plan = _plan(tmp_path) spec = _spec(tmp_path) def fake_run(cmd, input=None, capture_output=True, text=True, timeout=30): result = MagicMock(returncode=0, stderr="") if cmd[1:3] == ["policy", "read"]: result.stdout = POLICY elif cmd[1:3] == ["read", "-field=role_id"]: result.stdout = "role-id-value\n" elif "-field=secret_id" in cmd: result.stdout = "secret-id-value\n" else: result.stdout = "" return result with patch("ops_mason.executor.subprocess.run", side_effect=fake_run) as run: objects = build_approle_kv_lane(plan, spec) assert objects["policy_name"] == "workload-kv-read-test-lane" assert objects["approle_name"] == "test-lane" assert objects["kv_path"] == "reins/test/openrouter" role_id_file = spec.delivery_dir / "role_id" secret_id_file = spec.delivery_dir / "secret_id" assert role_id_file.read_text() == "role-id-value\n" assert secret_id_file.read_text() == "secret-id-value\n" assert oct(role_id_file.stat().st_mode)[-3:] == "600" assert oct(secret_id_file.stat().st_mode)[-3:] == "600" # policy write call carried the HCL on stdin, not argv -- never in a log line # -- KV v2 shape: data/ and metadata/ sub-paths, not the bare path policy_call = next(c for c in run.call_args_list if c.args[0][:2] == ["bao", "policy"]) assert 'path "reins/data/test/openrouter"' in policy_call.kwargs["input"] assert 'path "reins/metadata/test/openrouter"' in policy_call.kwargs["input"] assert 'path "reins/test/openrouter"' not in policy_call.kwargs["input"] # audit record landed at the explicit path, metadata only, no role_id/secret_id values audit_text = spec.audit_log_path.read_text() assert "policy_name" in audit_text assert "role-id-value" not in audit_text assert "secret-id-value" not in audit_text def test_reuse_policy_does_not_rewrite_existing_policy(tmp_path) -> None: plan = _plan(tmp_path) spec = _spec(tmp_path) spec.reuse_policy = True spec.reuse_policy_sha256 = POLICY_HASH plan = _plan(tmp_path, spec=spec) def fake_run(cmd, input=None, capture_output=True, text=True, timeout=30): result = MagicMock(returncode=0, stderr="") if cmd[1:3] == ["policy", "read"]: result.stdout = POLICY elif cmd[1:3] == ["read", "-field=role_id"]: result.stdout = "role-id-value\n" elif "-field=secret_id" in cmd: result.stdout = "secret-id-value\n" else: result.stdout = "" return result with patch("ops_mason.executor.subprocess.run", side_effect=fake_run) as run: build_approle_kv_lane(plan, spec) bao_cmds = [c.args[0] for c in run.call_args_list] assert not any(cmd[:3] == ["bao", "policy", "write"] for cmd in bao_cmds) assert any(cmd[1:3] == ["write", "auth/approle/role/test-lane"] for cmd in bao_cmds) def test_build_appends_audit_record(tmp_path) -> None: plan = _plan(tmp_path) spec = _spec(tmp_path) def fake_run(cmd, input=None, capture_output=True, text=True, timeout=30): result = MagicMock(returncode=0, stderr="") result.stdout = "value\n" return result with ( patch("ops_mason.executor.subprocess.run", side_effect=fake_run), patch("ops_mason.executor.record_build") as record_build_mock, ): build_approle_kv_lane(plan, spec) record_build_mock.assert_called_once() kwargs = record_build_mock.call_args.kwargs assert kwargs["plan_id"] == "test-lane" assert kwargs["approved_by"] == "bernd" assert "policy_name" in kwargs["objects"] assert kwargs["log_path"] == spec.audit_log_path def test_bao_failure_raises_build_error(tmp_path) -> None: plan = _plan(tmp_path) spec = _spec(tmp_path) fake_result = MagicMock(returncode=1, stderr="permission denied", stdout="") with patch("ops_mason.executor.subprocess.run", return_value=fake_result): with pytest.raises(BuildError, match="output suppressed"): build_approle_kv_lane(plan, spec) def _kubernetes_spec(tmp_path) -> KubernetesKVSpec: return KubernetesKVSpec( policy_name="workload-kv-read-binky-qonto-api", role_name="external-secrets-rapp-qonto", policy_sha256=POLICY_HASH, service_account_names=("external-secrets",), service_account_namespaces=("external-secrets",), audit_log_path=tmp_path / "audit.jsonl", ) def test_kubernetes_lane_refusal_never_calls_bao(tmp_path) -> None: plan = _plan(tmp_path, status="reviewed", approved_by=None, approved_at=None) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_kubernetes_kv_lane(plan, _kubernetes_spec(tmp_path)) run.assert_not_called() def test_kubernetes_lane_builds_exact_service_account_binding(tmp_path) -> None: plan = _plan(tmp_path) spec = _kubernetes_spec(tmp_path) plan = _plan(tmp_path, spec=spec) result = MagicMock(returncode=0, stderr="", stdout=POLICY) with ( patch("ops_mason.executor.subprocess.run", return_value=result) as run, patch("ops_mason.executor.record_build") as audit, ): objects = build_kubernetes_kv_lane(plan, spec) command = run.call_args.args[0] assert command == [ "bao", "write", "auth/kubernetes/role/external-secrets-rapp-qonto", "bound_service_account_names=external-secrets", "bound_service_account_namespaces=external-secrets", "policies=workload-kv-read-binky-qonto-api", "ttl=15m", ] assert objects["kubernetes_role_name"] == "external-secrets-rapp-qonto" audit.assert_called_once() def test_kubernetes_lane_requires_nonempty_bindings(tmp_path) -> None: plan = _plan(tmp_path) spec = KubernetesKVSpec( policy_name="policy", role_name="role", policy_sha256=POLICY_HASH, service_account_names=(), service_account_namespaces=("external-secrets",), ) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused, match="explicit service account"): build_kubernetes_kv_lane(plan, spec) run.assert_not_called() def _edit_plan(plan, edit): text = plan.path.read_text().split("---", 2) data = yaml.safe_load(text[1]) edit(data) plan.path.write_text("---\n" + yaml.safe_dump(data) + "---" + text[2]) @pytest.mark.parametrize("field,value", [ ("kv_path", "reins/another/openrouter"), ("policy_name", "other-policy"), ("approle_name", "other-role"), ("token_num_uses", 0), ("token_ttl", "10m"), ("token_max_ttl", "1h"), ("secret_id_ttl", "15m"), ("bao_bin", "another-bao"), ]) def test_changed_build_scope_refused_before_bao(tmp_path, field, value): spec = _spec(tmp_path) plan = _plan(tmp_path, spec=spec) setattr(spec, field, value) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused, match="approved specification"): build_approle_kv_lane(plan, spec) run.assert_not_called() assert not spec.delivery_dir.exists() def test_destination_is_part_of_approval(tmp_path): plan = _plan(tmp_path) spec = _spec(tmp_path) spec.delivery_dir = tmp_path / "elsewhere" with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused, match="approved specification"): build_approle_kv_lane(plan, spec) run.assert_not_called() @pytest.mark.parametrize("edit", [ lambda d: d.update(status="reviewed"), lambda d: d.update(approved_spec_sha256="0" * 64), lambda d: d.pop("build_spec"), lambda d: d.update(credential_type="openbao-kubernetes-kv"), lambda d: d["build_spec"].update(token_num_uses=0), ]) def test_changed_or_legacy_plan_refused_even_with_previously_loaded_approval(tmp_path, edit): plan = _plan(tmp_path) _edit_plan(plan, edit) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_approle_kv_lane(plan, _spec(tmp_path)) run.assert_not_called() @pytest.mark.parametrize("field,value", [ ("kv_path", "reins/test/*"), ("kv_path", "reins/test/+"), ("kv_path", "reins/test/"), ("kv_path", "reins/../test"), ("kv_path", 'reins/test/" { capabilities = ["sudo"] }'), ("kv_path", "reins//test"), ("kv_path", "reins"), ("policy_name", "a,b"), ("approle_name", "../other"), ("kv_capabilities", ("read", "sudo")), ("kv_capabilities", ("create", "read")), ("token_num_uses", True), ("token_num_uses", -1), ("token_ttl", "0"), ("token_max_ttl", "1s"), ("delivery_dir", None), ("reuse_policy", "false"), ]) def test_unsafe_spec_is_rejected_before_any_command(tmp_path, field, value): plan = _plan(tmp_path) spec = _spec(tmp_path) setattr(spec, field, value) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_approle_kv_lane(plan, spec) run.assert_not_called() @pytest.mark.parametrize("bindings", [("*",), ("a,b",), ("../a",), ("valid", "valid")]) def test_kubernetes_wildcard_or_ambiguous_bindings_refused(tmp_path, bindings): spec = _kubernetes_spec(tmp_path) plan = _plan(tmp_path, spec=spec) spec.service_account_names = bindings with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_kubernetes_kv_lane(plan, spec) run.assert_not_called() def test_kubernetes_valid_but_unapproved_binding_refused(tmp_path): spec = _kubernetes_spec(tmp_path) plan = _plan(tmp_path, spec=spec) spec.service_account_namespaces = ("other",) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused, match="approved specification"): build_kubernetes_kv_lane(plan, spec) run.assert_not_called() @pytest.mark.parametrize("engine", ["approle", "kubernetes"]) def test_changed_reused_policy_refuses_before_mutation(tmp_path, engine): if engine == "approle": spec = _spec(tmp_path) spec.reuse_policy, spec.reuse_policy_sha256 = True, POLICY_HASH build = build_approle_kv_lane else: spec = _kubernetes_spec(tmp_path) build = build_kubernetes_kv_lane plan = _plan(tmp_path, spec=spec) with patch("ops_mason.executor.subprocess.run", return_value=MagicMock(returncode=0, stdout=POLICY + "# changed")) as run: with pytest.raises(BuildRefused, match="content pin"): build(plan, spec) assert len(run.call_args_list) == 1 assert run.call_args.args[0][1:3] == ["policy", "read"] def test_credentials_are_private_before_first_bao_call_even_with_open_umask(tmp_path): plan, spec = _plan(tmp_path), _spec(tmp_path) def fake_run(*args, **kwargs): assert stat.S_IMODE(spec.delivery_dir.stat().st_mode) == 0o700 for name in ("role_id", "secret_id"): assert stat.S_IMODE((spec.delivery_dir / name).stat().st_mode) == 0o600 return MagicMock(returncode=0, stdout="synthetic-credential\n", stderr="") previous = os.umask(0) try: with patch("ops_mason.executor.subprocess.run", side_effect=fake_run): build_approle_kv_lane(plan, spec) finally: os.umask(previous) assert "synthetic-credential" not in spec.audit_log_path.read_text() @pytest.mark.parametrize("case", ["existing", "symlink-file", "symlink-directory", "symlink-parent", "public-directory", "hardlink"]) def test_unsafe_delivery_refuses_without_mutation_or_overwrite(tmp_path, case): spec = _spec(tmp_path) victim = tmp_path / "victim" victim.write_text("leave untouched") if case == "symlink-directory": target = tmp_path / "target" target.mkdir(mode=0o700) spec.delivery_dir.symlink_to(target, target_is_directory=True) elif case == "symlink-parent": parent = tmp_path / "parent" parent.symlink_to(tmp_path, target_is_directory=True) spec.delivery_dir = parent / "delivery" else: spec.delivery_dir.mkdir(mode=0o700) if case == "public-directory": spec.delivery_dir.chmod(0o755) elif case == "existing": (spec.delivery_dir / "secret_id").write_text("existing-credential") elif case == "hardlink": os.link(victim, spec.delivery_dir / "secret_id") else: (spec.delivery_dir / "secret_id").symlink_to(victim) plan = _plan(tmp_path, spec=spec) with patch("ops_mason.executor.subprocess.run") as run: with pytest.raises(BuildRefused): build_approle_kv_lane(plan, spec) run.assert_not_called() assert victim.read_text() == "leave untouched" if case == "existing": assert (spec.delivery_dir / "secret_id").read_text() == "existing-credential" @pytest.mark.parametrize("failure", ["exit", "timeout"]) def test_command_errors_never_echo_credentials(tmp_path, failure): plan, spec = _plan(tmp_path), _spec(tmp_path) marker = "synthetic-sensitive-output" kwargs = ({"return_value": MagicMock(returncode=1, stdout=marker, stderr=marker)} if failure == "exit" else {"side_effect": subprocess.TimeoutExpired(["bao"], 30, output=marker, stderr=marker)}) with patch("ops_mason.executor.subprocess.run", **kwargs): with pytest.raises(BuildError) as error: build_approle_kv_lane(plan, spec) assert marker not in str(error.value) assert not spec.audit_log_path.exists() assert stat.S_IMODE((spec.delivery_dir / "secret_id").stat().st_mode) == 0o600