import copy import json from types import SimpleNamespace import pytest from secrets_engine.catalog import validate_entry from secrets_engine.config import Config from secrets_engine.errors import BackendError, DecisionError, ProvisioningError from secrets_engine.openbao import OpenBaoClient from secrets_engine.wrap import normalize_wrap_ttl, write_wrapped_handoff from tests.test_catalog import VALID WRAP_TOKEN = "wrap-token-SUPER-SECRET" WRAP_ACCESSOR = "wrap-accessor-value" class FakeWrapClient: def __init__(self): self.calls = [] def wrap_kv_get(self, mount, path, *, ttl): from secrets_engine.openbao import WrappedResponse, accessor_fingerprint self.calls.append(("kv", mount, path, ttl)) return WrappedResponse( wrap_token=WRAP_TOKEN, accessor_fingerprint=accessor_fingerprint(WRAP_ACCESSOR), ttl=ttl, creation_path=f"{mount}/{path}", ) def wrap_approle_secret_id(self, role_name, *, ttl): from secrets_engine.openbao import WrappedResponse, accessor_fingerprint self.calls.append(("approle", role_name, ttl)) return WrappedResponse( wrap_token=WRAP_TOKEN, accessor_fingerprint=accessor_fingerprint(WRAP_ACCESSOR), ttl=ttl, creation_path=f"auth/approle/role/{role_name}/secret-id", ) def test_normalize_wrap_ttl_bounds(): assert normalize_wrap_ttl("15m") == "15m" assert normalize_wrap_ttl("60s") == "60s" with pytest.raises(ProvisioningError, match="15m"): normalize_wrap_ttl("16m") with pytest.raises(ProvisioningError, match="like"): normalize_wrap_ttl("15") def test_wrap_kv_writes_0600_file_and_omits_token_from_result(tmp_path): out = tmp_path / "wrap.token" entry = validate_entry(copy.deepcopy(VALID)) result = write_wrapped_handoff( FakeWrapClient(), entry, out_file=out, ttl="15m" ) assert out.read_text().strip() == WRAP_TOKEN assert (out.stat().st_mode & 0o077) == 0 assert result.wrap_handle assert WRAP_TOKEN not in result.wrap_handle assert WRAP_TOKEN not in result.out_file assert WRAP_ACCESSOR not in result.wrap_handle def test_wrap_rejects_repo_paths(): entry = validate_entry(copy.deepcopy(VALID)) with pytest.raises(ProvisioningError, match="Git worktree"): write_wrapped_handoff( FakeWrapClient(), entry, out_file=__import__("pathlib").Path("wrap.token"), ttl="15m", ) def test_parse_wrap_rejects_unwrapped_secret_payload(): client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao") secret_json = json.dumps( {"data": {"data": {"api_token": "npm_SHOULDNEVERPARSE"}}} ) with pytest.raises(BackendError, match="was not applied") as raised: client._parse_wrap_response( secret_json, ttl="15m", creation_path="secret/x" ) assert "npm_SHOULDNEVERPARSE" not in str(raised.value) def test_parse_wrap_accepts_wrap_info_only(): client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao") payload = json.dumps( { "wrap_info": { "token": WRAP_TOKEN, "accessor": WRAP_ACCESSOR, "ttl": 900, } } ) wrapped = client._parse_wrap_response( payload, ttl="15m", creation_path="secret/x" ) assert wrapped.wrap_token == WRAP_TOKEN assert WRAP_ACCESSOR not in wrapped.accessor_fingerprint assert WRAP_TOKEN not in repr(wrapped) def test_wrap_kv_get_uses_wrap_ttl_flag(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao") seen = {} def fake_run(args, stdin=None): seen["args"] = list(args) return SimpleNamespace( returncode=0, stdout=json.dumps( {"wrap_info": {"token": WRAP_TOKEN, "accessor": WRAP_ACCESSOR}} ), stderr="", ) monkeypatch.setattr(client, "_run", fake_run) wrapped = client.wrap_kv_get("secret", "test/team/thing", ttl="15m") assert "-wrap-ttl=15m" in seen["args"] assert WRAP_TOKEN not in " ".join(seen["args"]) assert wrapped.wrap_token == WRAP_TOKEN def test_production_wrap_fails_closed(tmp_path, monkeypatch): from secrets_engine import cli data = copy.deepcopy(VALID) data.update(stage="prod", approval={"model": "decision", "decision_ref": "x"}) entry = validate_entry(data) monkeypatch.setattr(cli, "get_entry", lambda *_args: entry) monkeypatch.delenv("SECRETS_ENGINE_UNSAFE_DEMO", raising=False) monkeypatch.setattr( cli.OpenBaoClient, "resolve", lambda *_args, **_kwargs: pytest.fail("backend must not be reached"), ) cfg = Config( catalog_dir=tmp_path, policy_dir=tmp_path, evidence_dir=tmp_path / "evidence", hub_url="http://127.0.0.1:8000", bao_addr="http://127.0.0.1:8200", topic_id="test-topic", ) args = SimpleNamespace( catalog_id=entry.id, out=str(tmp_path / "wrap.token"), ttl="15m", bootstrap_token_file=None, auth="auto", ) with pytest.raises(DecisionError, match="production action 'wrap'"): cli.cmd_wrap(cfg, args)