secrets-engine/tests/test_human_control.py

171 lines
7.8 KiB
Python
Raw Normal View History

"""A declared human-control lane cannot use an undeclared approval fact."""
import copy
import json
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
from secrets_engine import cli
from secrets_engine.approval_consume import (
ConsumeBinding, authorize_action, require_production_consume,
)
from secrets_engine.authorization import build_action_request, request_digest
from secrets_engine.catalog import load_entry, validate_entry
from secrets_engine.errors import CatalogError, DecisionError
from tests.authorization_stub import AuthorizationStub
from tests.test_catalog import VALID
from tests.test_consume_binding_join import (
AUTH_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
)
from tests.test_exec_owner import bound
from tests.test_privileged_cli_evidence import _config, _records
def controlled_entry():
entry = _entry()
entry.approval.update(model="decision", human_control=True)
return entry
@pytest.mark.parametrize("value", [None, 0, 1, "true", "false", [], {}])
def test_catalog_declaration_is_strictly_boolean(value):
raw = copy.deepcopy(VALID)
raw["approval"].update(model="decision", human_control=value)
with pytest.raises(CatalogError, match="human_control"):
validate_entry(raw)
def test_bootstrap_only_cannot_declare_a_human_control():
raw = copy.deepcopy(VALID)
raw["approval"]["human_control"] = True
with pytest.raises(CatalogError, match="human_control"):
validate_entry(raw)
@pytest.mark.parametrize("value", ["missing", False, None, 1, "true", [], {}])
def test_undeclared_or_malformed_claim_refuses_before_pdp(tmp_path, value):
entry = controlled_entry()
claim = _served(entry)
if value != "missing":
claim["binding"]["human_control"] = value
claim["entries"] = [{"subject_id": "human:fixture", "principal_type": "human"}]
with pytest.raises(DecisionError, match="human_control"):
authorize_action(_Cfg(_token(tmp_path)), entry, "deactivate", fields=("api_token",),
opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not call PDP"))
def test_human_control_intent_changes_the_submitted_request_binding():
ordinary = _entry()
controlled = controlled_entry()
def request(entry):
return build_action_request(entry, "deactivate", subject_id="service:fixture",
subject_type="Service", purpose="same-action", fields=("api_token",))
before, after = request(ordinary), request(controlled)
assert "human_control" not in before["context"]
assert after["context"]["human_control"] is True
assert request_digest(before) != request_digest(after)
def test_declared_fact_yields_a_human_control_consume_binding(tmp_path):
entry = controlled_entry()
claim = _served(entry)
claim["binding"]["human_control"] = True
authorized = _resolve(_Cfg(_token(tmp_path)), entry, claim)
assert authorized.binding.human_control is True
assert authorized.as_evidence()["approval_human_control"] is True
def test_legacy_lane_still_accepts_undeclared_claim(tmp_path):
entry = _entry()
result = _resolve(_Cfg(_token(tmp_path)), entry, _served(entry))
assert result is not None
assert result.binding.human_control is False
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_missing_serving_path_cannot_fall_back_to_lane_or_demo_approval(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
authorize_action(_Cfg(None, approval_url="", approval_token_file=None), entry, "exec")
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_consume_requires_observed_human_control_even_with_demo_enabled(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
require_production_consume(_config(tmp_path), entry,
binding=ConsumeBinding(AUTH_ID, "sha256:"+"a"*64),
opener=lambda *a, **k: pytest.fail("must not consume"))
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path))
binding = ConsumeBinding(AUTH_ID, "sha256:"+"a"*64, human_control=True)
calls = []
def consume(request, **kwargs):
calls.append(request.full_url)
response = _opener({"status": "consumed", "request_digest": binding.request_digest})(request)
response.getcode = lambda: 200
return response
result = require_production_consume(cfg, entry, binding=binding, opener=consume)
assert result.approval_id == AUTH_ID
assert calls == [f"https://approval.example/v1/approvals/{AUTH_ID}/consume"]
def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
from secrets_engine import approval_consume
entry = controlled_entry()
claim = _served(entry)
claim["binding"]["human_control"] = True
monkeypatch.setattr(approval_consume, "fetch_approval_claim", lambda **k: claim)
cfg = _Cfg(_token(tmp_path))
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package,
version=cfg.authorization_policy_version)
original_validate = approval_consume.validate_decision_envelope
def invalidate_after_check(*args, **kwargs):
result = original_validate(*args, **kwargs)
claim["binding"]["human_control"] = False
return result
monkeypatch.setattr(approval_consume, "validate_decision_envelope", invalidate_after_check)
def check(request, **kwargs):
return _opener(pdp.decision(json.loads(request.data)))(request)
with pytest.raises(DecisionError, match="human_control"):
authorize_action(cfg, entry, "deactivate", fields=("api_token",),
policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,), pdp_opener=check)
def test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child(bound, tmp_path, monkeypatch):
raw, _, _ = bound
raw["stage"] = "prod"
raw["approval"].update(model="decision", authorization_id=AUTH_ID,
purpose="contract-test", human_control=True)
entry = validate_entry(raw)
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path), authorization_subject_id="user:alice",
authorization_subject_type="Human")
claim = _served(entry, action="exec")
monkeypatch.setattr(cli, "get_entry", lambda *a: entry)
monkeypatch.setattr(cli, "authorize_action", lambda *a, **k: authorize_action(*a, **k,
opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not check")))
for name in ("require_production_consume", "_open_backend"):
monkeypatch.setattr(cli, name, lambda *a, **k: pytest.fail("must not consume or open backend"))
with pytest.raises(DecisionError, match="human_control"):
cli.cmd_exec(cfg, SimpleNamespace(catalog=entry.id, field="api_token", mode="exec-env",
command=raw["delivery_config"]["exec_owner"]["command"]))
records = _records(tmp_path)
assert [r["result"] for r in records] == ["attempt", "failed-DecisionError"]
def test_factory_catalog_declares_human_control_and_keeps_owner_pending():
entry = load_entry(Path(__file__).resolve().parents[1]/"catalog/glas-claude-agent-dev-anthropic.yaml")
assert entry.approval["human_control"] is True
assert entry.delivery_config["exec_owner"]["status"] == "pending"