Harden secret provisioning and lifecycle controls
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
This commit is contained in:
parent
0617923ff1
commit
3a1bd4f1c8
23 changed files with 1369 additions and 162 deletions
138
tests/test_multifield_readiness.py
Normal file
138
tests/test_multifield_readiness.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import copy
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from secrets_engine import cli
|
||||
from secrets_engine.catalog import validate_entry
|
||||
from secrets_engine.config import Config
|
||||
from secrets_engine.errors import PolicyGuardError
|
||||
from secrets_engine.routing import route_lane
|
||||
from secrets_engine.verify import VerifyResult
|
||||
from tests.test_catalog import VALID
|
||||
|
||||
|
||||
def _entry():
|
||||
data = copy.deepcopy(VALID)
|
||||
data["fields"] = ["api_token", "webhook_secret"]
|
||||
return validate_entry(data)
|
||||
|
||||
|
||||
class ReadinessClient:
|
||||
def __init__(self, presence):
|
||||
self.presence = presence
|
||||
self.requested_fields = None
|
||||
|
||||
def is_reachable(self):
|
||||
return True
|
||||
|
||||
def read_policy(self, _name):
|
||||
return "path \"secret/data/test/team/thing\" {}"
|
||||
|
||||
def approle_exists(self, _name):
|
||||
return True
|
||||
|
||||
def kv_fields_present(self, _mount, _path, fields):
|
||||
self.requested_fields = list(fields)
|
||||
return dict(self.presence)
|
||||
|
||||
|
||||
def test_route_requires_every_declared_field_and_names_only_missing_fields(tmp_path):
|
||||
client = ReadinessClient({"api_token": True, "webhook_secret": False})
|
||||
|
||||
result = route_lane(_entry(), hub_url="", repo_root=tmp_path, client=client)
|
||||
|
||||
assert client.requested_fields == ["api_token", "webhook_secret"]
|
||||
assert result.value_present is False
|
||||
assert result.ready is False
|
||||
assert result.missing_fields == ["webhook_secret"]
|
||||
assert result.missing == "provisioned secret fields: webhook_secret"
|
||||
assert "--field webhook_secret" in result.next_command
|
||||
|
||||
|
||||
def test_route_is_ready_only_when_every_declared_field_is_present(tmp_path):
|
||||
client = ReadinessClient({"api_token": True, "webhook_secret": True})
|
||||
|
||||
result = route_lane(_entry(), hub_url="", repo_root=tmp_path, client=client)
|
||||
|
||||
assert result.value_present is True
|
||||
assert result.missing_fields == []
|
||||
assert result.ready is True
|
||||
|
||||
|
||||
def _config(tmp_path: Path) -> Config:
|
||||
return Config(
|
||||
catalog_dir=tmp_path,
|
||||
policy_dir=tmp_path,
|
||||
evidence_dir=tmp_path / "evidence",
|
||||
hub_url="",
|
||||
bao_addr="http://127.0.0.1:8200",
|
||||
topic_id="test-topic",
|
||||
)
|
||||
|
||||
|
||||
def test_verify_defaults_to_every_declared_field_and_one_path_denial(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
entry = _entry()
|
||||
calls = []
|
||||
records = []
|
||||
|
||||
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
|
||||
monkeypatch.setattr(cli, "_require_lane_approval", lambda *_args: None)
|
||||
monkeypatch.setattr(cli.OpenBaoClient, "resolve", lambda *_args, **_kwargs: object())
|
||||
|
||||
def fake_verify(_client, _entry, field, *, positive, negative):
|
||||
calls.append((field, positive, negative))
|
||||
check = "positive" if positive else "negative"
|
||||
return [VerifyResult(check, True, {"field": field, "reason": "test"})]
|
||||
|
||||
monkeypatch.setattr(cli, "run_verification", fake_verify)
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"_writer",
|
||||
lambda _cfg: SimpleNamespace(record=lambda *args, **kwargs: records.append((args, kwargs))),
|
||||
)
|
||||
args = SimpleNamespace(
|
||||
catalog_id=entry.id,
|
||||
bootstrap_token_file=None,
|
||||
field=None,
|
||||
positive=False,
|
||||
negative=False,
|
||||
)
|
||||
|
||||
assert cli.cmd_verify(_config(tmp_path), args) == 0
|
||||
assert calls == [
|
||||
("api_token", True, False),
|
||||
("webhook_secret", True, False),
|
||||
("api_token", False, True),
|
||||
]
|
||||
assert len(records) == 3
|
||||
|
||||
|
||||
def test_live_destroy_fails_before_approval_or_backend_until_action_contract(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
entry = _entry()
|
||||
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"_require_lane_approval",
|
||||
lambda *_args: pytest.fail("coarse lane approval must not authorize destroy"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli.OpenBaoClient,
|
||||
"resolve",
|
||||
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
|
||||
)
|
||||
args = SimpleNamespace(
|
||||
catalog_id=entry.id,
|
||||
operation="destroy",
|
||||
dry_run=False,
|
||||
confirm_destroy=entry.id,
|
||||
bootstrap_token_file=None,
|
||||
)
|
||||
|
||||
with pytest.raises(PolicyGuardError, match="exact-action destruction approval"):
|
||||
cli.cmd_lifecycle(_config(tmp_path), args)
|
||||
Loading…
Add table
Add a link
Reference in a new issue