Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
167 lines
8.7 KiB
Python
167 lines
8.7 KiB
Python
import copy
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from secrets_engine import cli, exec_delivery
|
|
from secrets_engine.authorization import build_action_request, request_digest, validate_decision_envelope
|
|
from secrets_engine.catalog import load_entry, validate_entry
|
|
from secrets_engine.errors import CatalogError, DecisionError, DeliveryError
|
|
from secrets_engine.exec_owner import owner_digest
|
|
from tests.test_action_authorization import _envelope
|
|
from tests.test_catalog import VALID
|
|
from tests.test_lane_state import _cfg
|
|
|
|
|
|
@pytest.fixture
|
|
def bound(tmp_path):
|
|
private = tmp_path / "owner"
|
|
private.mkdir(mode=0o700)
|
|
executable = private / "dash"
|
|
shutil.copyfile("/bin/dash", executable)
|
|
executable.chmod(0o700)
|
|
script = private / "owner.sh"
|
|
script.write_text('''test "$API_TOKEN" = synthetic-owner-value || exit 10
|
|
test -z "$PARENT_CREDENTIAL" || exit 11
|
|
test -z "$BAO_TOKEN" || exit 12
|
|
test -z "$SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE" || exit 13
|
|
test -z "$PYTHONPATH" || exit 14
|
|
read ignored && exit 15
|
|
printf '%s\\n' "$API_TOKEN"
|
|
printf 'owner-child-ok\\n'
|
|
pwd
|
|
''')
|
|
script.chmod(0o600)
|
|
data = copy.deepcopy(VALID)
|
|
data["delivery_config"] = {"exec_owner": {
|
|
"status": "configured", "owner": "fixture-owner",
|
|
"command": [str(executable), str(script)], "cwd": str(private),
|
|
"environment": {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"},
|
|
"files": {str(f): {"sha256": hashlib.sha256(f.read_bytes()).hexdigest(), "private": True} for f in [executable, script]},
|
|
}}
|
|
return data, private, script
|
|
|
|
|
|
def test_actual_bound_child_gets_only_selected_secret_and_fixed_environment(bound, monkeypatch, capsys):
|
|
data, private, _ = bound
|
|
entry = validate_entry(data)
|
|
for name in ["PARENT_CREDENTIAL", "BAO_TOKEN", "SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE", "PYTHONPATH"]:
|
|
monkeypatch.setenv(name, "parent-sensitive-fixture")
|
|
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: "synthetic-owner-value")
|
|
assert exec_delivery.exec_with_secret(object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"], mode="exec-env", expected_owner_digest=owner_digest(entry)) == 0
|
|
output = capsys.readouterr().out
|
|
assert "owner-child-ok" in output and str(private) in output
|
|
assert "synthetic-owner-value" not in output and "parent-sensitive-fixture" not in output
|
|
assert os.environ["BAO_TOKEN"] == "parent-sensitive-fixture"
|
|
|
|
|
|
@pytest.mark.parametrize("change", ["command", "extra_arg", "config", "symlink", "private_mode", "parent_mode", "mode", "field"])
|
|
def test_bound_refusals_happen_before_fetch(bound, monkeypatch, change):
|
|
data, private, script = bound
|
|
entry = validate_entry(data)
|
|
command = list(data["delivery_config"]["exec_owner"]["command"])
|
|
mode, field = "exec-env", "api_token"
|
|
if change == "command": command[0] = "/bin/sh"
|
|
elif change == "extra_arg": command.append("--other")
|
|
elif change == "config": script.write_text("changed")
|
|
elif change == "symlink":
|
|
saved = script.with_suffix(".saved")
|
|
script.rename(saved); script.symlink_to(saved)
|
|
elif change == "private_mode": script.chmod(0o640)
|
|
elif change == "parent_mode": private.chmod(0o770)
|
|
elif change == "mode": mode = "exec-file"
|
|
elif change == "field": field = "wrong"
|
|
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("must refuse before read"))
|
|
with pytest.raises(DeliveryError):
|
|
exec_delivery.exec_with_secret(object(), entry, field, command, mode=mode)
|
|
|
|
|
|
def test_config_changed_during_fetch_does_not_launch(bound, monkeypatch):
|
|
data, _, script = bound
|
|
entry = validate_entry(data)
|
|
def fetch(*a, **k):
|
|
script.write_text("changed during fetch")
|
|
return "synthetic-owner-value"
|
|
monkeypatch.setattr(exec_delivery, "_fetch_value", fetch)
|
|
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("must not launch"))
|
|
with pytest.raises(DeliveryError, match="digest"):
|
|
exec_delivery.exec_with_secret(object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"])
|
|
|
|
|
|
def test_binding_changed_after_approval_refuses_before_fetch(bound, monkeypatch):
|
|
data, _, _ = bound
|
|
entry = validate_entry(data)
|
|
expected = owner_digest(entry)
|
|
entry.delivery_config["exec_owner"]["environment"]["LANG"] = "C"
|
|
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("must not read"))
|
|
with pytest.raises(DeliveryError, match="after action admission"):
|
|
exec_delivery.exec_with_secret(object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"], expected_owner_digest=expected)
|
|
|
|
|
|
def test_pending_real_catalog_refuses_before_approval_and_backend(tmp_path, monkeypatch):
|
|
entry = load_entry(Path(__file__).resolve().parents[1] / "catalog/glas-claude-agent-dev-anthropic.yaml")
|
|
monkeypatch.setattr(cli, "get_entry", lambda *a: entry)
|
|
for name in ["_require_lane_approval", "_open_backend"]:
|
|
monkeypatch.setattr(cli, name, lambda *a, **k: pytest.fail("no approval consume or backend"))
|
|
with pytest.raises(DeliveryError, match="pending"):
|
|
cli.cmd_exec(_cfg(tmp_path), SimpleNamespace(field=None, catalog=entry.id, command=["/bin/echo"], mode="exec-env"))
|
|
|
|
|
|
def test_configured_command_refusal_precedes_approval_and_backend(bound, monkeypatch, tmp_path):
|
|
data, _, _ = bound
|
|
entry = validate_entry(data)
|
|
monkeypatch.setattr(cli, "get_entry", lambda *a: entry)
|
|
for name in ["_require_lane_approval", "_open_backend"]:
|
|
monkeypatch.setattr(cli, name, lambda *a, **k: pytest.fail("no approval consume or backend"))
|
|
with pytest.raises(DeliveryError, match="catalog-bound"):
|
|
cli.cmd_exec(_cfg(tmp_path), SimpleNamespace(field=None, catalog=entry.id, command=["/bin/echo"], mode="exec-env"))
|
|
|
|
|
|
@pytest.mark.parametrize("changed", ["command", "cwd", "environment", "files", "owner"])
|
|
def test_changed_recipient_cannot_replay_prior_decision(bound, changed):
|
|
data, private, script = bound
|
|
entry = validate_entry(data)
|
|
def request(e): return build_action_request(e, "exec", subject_id="agent:fixture", subject_type="Agent", purpose="owner-proof", fields=["api_token"], request_id="fixture-owner-proof")
|
|
before = request(entry)
|
|
envelope = _envelope(before)
|
|
other = copy.deepcopy(data)
|
|
binding = other["delivery_config"]["exec_owner"]
|
|
if changed == "command": binding["command"].append("--other")
|
|
elif changed == "cwd": binding["cwd"] = str(private / "other")
|
|
elif changed == "environment": binding["environment"]["LANG"] = "C"
|
|
elif changed == "files": binding["files"][str(script)]["sha256"] = "1" * 64
|
|
else: binding["owner"] = "another-owner"
|
|
after = request(validate_entry(other))
|
|
assert request_digest(before) != request_digest(after)
|
|
with pytest.raises(DecisionError):
|
|
validate_decision_envelope(envelope, after, accepted_policy_packages={"secrets-engine.catalog-lane.lifecycle"}, accepted_policy_versions={"v2"})
|
|
assert "exec_owner_sha256" not in request(validate_entry(VALID))["context"]
|
|
|
|
|
|
@pytest.mark.parametrize("change", ["null", "unknown", "relative", "missing_pin", "null_private", "preload", "credential", "other_mode"])
|
|
def test_invalid_binding_is_not_a_catalog_fallback(bound, change):
|
|
data, _, _ = bound
|
|
binding = data["delivery_config"]["exec_owner"]
|
|
if change == "null": data["delivery_config"]["exec_owner"] = None
|
|
elif change == "unknown": binding["extra"] = True
|
|
elif change == "relative": binding["command"][0] = "python3"
|
|
elif change == "missing_pin": binding["files"] = {}
|
|
elif change == "null_private": next(iter(binding["files"].values()))["private"] = None
|
|
elif change == "preload": binding["environment"]["LD_PRELOAD"] = "/tmp/loader"
|
|
elif change == "credential": binding["environment"]["SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE"] = "/tmp/credential"
|
|
else: data["delivery_modes"].append("exec-file")
|
|
with pytest.raises(CatalogError): validate_entry(data)
|
|
|
|
|
|
def test_pending_owner_never_advertises_ready(tmp_path, monkeypatch):
|
|
from secrets_engine import routing
|
|
entry = load_entry(Path(__file__).resolve().parents[1] / "catalog/glas-claude-agent-dev-anthropic.yaml")
|
|
monkeypatch.setattr(routing, "resolve_decision", lambda **k: SimpleNamespace(status="approved", review_url="", is_approved=lambda: True))
|
|
client = SimpleNamespace(is_reachable=lambda: True, read_policy=lambda p: "policy", approle_exists=lambda r: True, kv_fields_present=lambda *a: {"ANTHROPIC_API_KEY": True})
|
|
result = routing.route_lane(entry, hub_url="", repo_root=tmp_path, client=client)
|
|
assert not result.ready and "exec owner" in result.missing
|
|
assert "<command" not in result.next_command
|