secrets-engine/tests/test_wrap.py
tegwick afd1c8e593
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Add response-wrapped operator handoff
secrets-engine wrap writes a single-use OpenBao wrap token to a mode-0600
out-of-repo file and never prints it. KV reads and AppRole secret_ids are
wrapped with a 15m TTL cap. Unwrapped secret payloads fail closed.
Production wrap remains fail-closed.

Assistant: grok
Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
2026-09-02 08:12:42 +02:00

160 lines
5.3 KiB
Python

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)