check_fetch_policy now posts only to policy.fetch_flex_auth_url (access-engine pin flex-auth-ops-warden-fetch, workstation 127.0.0.1:19091). Signing keeps policy.flex_auth_url. Unset or unreachable fetch evaluator fails closed with a message naming it; no fallback to the signing pin or to allow. warden access advisory output names the fetch evaluator. Recorded under WARDEN-WP-0039-T03. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3775@bnt-lap001 Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
"""Secret-read checks use their own evaluator pin, never the SSH signing pin (CUST-WP-0075)."""
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from warden.access import policy_gate_status
|
|
from warden.ca import CAError
|
|
from warden.config import PolicyConfig, load_config
|
|
from warden.models import ActorType, CertSpec
|
|
from warden.policy import check_fetch_policy, check_sign_policy
|
|
|
|
SIGN_URL = "http://127.0.0.1:19090"
|
|
FETCH_URL = "http://127.0.0.1:19091"
|
|
|
|
|
|
def _allow(url, json=None, headers=None, timeout=None):
|
|
return httpx.Response(
|
|
200,
|
|
json={"effect": "allow", "id": "decision:fetch", "reason": "secret_read_lane_matched"},
|
|
request=httpx.Request("POST", url),
|
|
)
|
|
|
|
|
|
def _fetch(cfg):
|
|
return check_fetch_policy(
|
|
cfg, need_id="forgejo-admin-api-token", owner_repo="railiance-platform", domain=None
|
|
)
|
|
|
|
|
|
def test_fetch_check_goes_to_fetch_pin_never_sign_pin():
|
|
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
|
with patch("warden.policy.httpx.post", side_effect=_allow) as post:
|
|
assert _fetch(cfg) == "decision:fetch"
|
|
urls = [c.args[0] for c in post.call_args_list]
|
|
assert urls == [f"{FETCH_URL}/v1/check"]
|
|
body = post.call_args.kwargs["json"]
|
|
assert body["resource"]["system"] == "railiance-platform"
|
|
assert body["action"] == "read"
|
|
|
|
|
|
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
|
def test_unset_fetch_url_fails_closed_without_using_sign_pin(failure_mode):
|
|
cfg = PolicyConfig(flex_auth_url=SIGN_URL)
|
|
cfg.failure_modes["unknown"] = failure_mode
|
|
with patch("warden.policy.httpx.post") as post:
|
|
with pytest.raises(CAError, match="policy.fetch_flex_auth_url") as err:
|
|
_fetch(cfg)
|
|
assert "secret-read evaluator not configured" in str(err.value)
|
|
post.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
|
def test_unreachable_fetch_url_fails_closed(failure_mode):
|
|
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
|
cfg.failure_modes["unknown"] = failure_mode
|
|
calls = []
|
|
|
|
def boom(url, **kwargs):
|
|
calls.append(url)
|
|
raise httpx.ConnectError("refused", request=httpx.Request("POST", url))
|
|
|
|
with patch("warden.policy.httpx.post", side_effect=boom):
|
|
with pytest.raises(CAError, match="flex-auth-ops-warden-fetch\\) unreachable"):
|
|
_fetch(cfg)
|
|
assert calls == [f"{FETCH_URL}/v1/check"]
|
|
|
|
|
|
def test_fetch_403_keeps_existing_refusal_text():
|
|
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
|
response = httpx.Response(403, request=httpx.Request("POST", f"{FETCH_URL}/v1/check"))
|
|
with patch("warden.policy.httpx.post", return_value=response):
|
|
with pytest.raises(CAError) as err:
|
|
_fetch(cfg)
|
|
assert str(err.value) == (
|
|
"flex-auth refused fetch policy check (HTTP 403); "
|
|
"caller authentication or system binding was rejected"
|
|
)
|
|
|
|
|
|
def test_sign_check_still_uses_sign_pin(tmp_path):
|
|
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
|
pub = tmp_path / "id.pub"
|
|
pub.write_text("ssh-ed25519 AAAA test\n")
|
|
spec = CertSpec(actor_name="agt-example", actor_type=ActorType.AGT,
|
|
pubkey_path=pub, ttl_hours=1, principals=["agt"])
|
|
with patch("warden.policy.httpx.post", side_effect=_allow) as post:
|
|
assert check_sign_policy(cfg, spec) == "decision:fetch"
|
|
assert [c.args[0] for c in post.call_args_list] == [f"{SIGN_URL}/v1/check"]
|
|
assert post.call_args.kwargs["json"]["resource"]["system"] == "ops-warden"
|
|
|
|
|
|
def _write_cfg(tmp_path, monkeypatch, policy_block):
|
|
cfg = tmp_path / "warden.yaml"
|
|
cfg.write_text(
|
|
"backend: local\nca_key: /tmp/none\n" + policy_block
|
|
)
|
|
monkeypatch.setenv("WARDEN_CONFIG", str(cfg))
|
|
return cfg
|
|
|
|
|
|
def test_config_loads_fetch_url(tmp_path, monkeypatch):
|
|
_write_cfg(tmp_path, monkeypatch,
|
|
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
|
cfg = load_config()
|
|
assert cfg.policy.flex_auth_url == SIGN_URL
|
|
assert cfg.policy.fetch_flex_auth_url == FETCH_URL
|
|
|
|
|
|
def test_gate_status_names_fetch_evaluator(tmp_path, monkeypatch):
|
|
_write_cfg(tmp_path, monkeypatch,
|
|
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
|
status = policy_gate_status()
|
|
assert "flex-auth-ops-warden-fetch" in status and FETCH_URL in status
|
|
assert SIGN_URL not in status
|
|
|
|
|
|
def test_gate_status_unset_fetch_url_says_refused(tmp_path, monkeypatch):
|
|
_write_cfg(tmp_path, monkeypatch, f"policy:\n flex_auth_url: {SIGN_URL}\n")
|
|
status = policy_gate_status()
|
|
assert "fetch_flex_auth_url unset" in status and "refused" in status
|
|
|
|
|
|
def test_access_advisory_prints_fetch_evaluator(tmp_path, monkeypatch):
|
|
from pathlib import Path
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from warden.cli import app
|
|
|
|
catalog = Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml"
|
|
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(catalog))
|
|
_write_cfg(tmp_path, monkeypatch,
|
|
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
|
result = CliRunner().invoke(app, ["access", "forgejo-admin-api-token"])
|
|
assert result.exit_code == 0, result.output
|
|
flat = " ".join(result.output.split())
|
|
assert "flex-auth-ops-warden-fetch" in flat and FETCH_URL in flat
|