2026-08-24 03:10:13 -03:00
|
|
|
"""HUX-12 evidence identity and configuration fail-closed tests."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation"
|
|
|
|
|
if str(FOUNDATION) not in sys.path:
|
|
|
|
|
sys.path.insert(0, str(FOUNDATION))
|
|
|
|
|
|
|
|
|
|
from hux import identity, release_security # noqa: E402
|
|
|
|
|
from hux.errors import Forbidden, Invalid, Unauthorized # noqa: E402
|
|
|
|
|
|
|
|
|
|
KEY = "evidence-key-value-that-is-at-least-32-bytes"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def policy() -> dict:
|
|
|
|
|
"""Return one complete non-secret release allowlist."""
|
|
|
|
|
return {
|
|
|
|
|
"schema": "hux.release_evidence_policy.v1",
|
|
|
|
|
"max_evidence_age_seconds": 900,
|
|
|
|
|
"workloads": {"hermes-webui": {
|
2026-09-01 20:43:33 -03:00
|
|
|
"review_url_prefix": "https://scm.bstein.dev/titan/atlas-iac/pulls/",
|
2026-08-24 03:10:13 -03:00
|
|
|
"jenkins_job_url": "https://jenkins.bstein.dev/job/hermes-webui-image",
|
|
|
|
|
"image_repository": "registry.bstein.dev/bstein/hermes-webui",
|
|
|
|
|
"flux_kustomization": "hermes",
|
|
|
|
|
"health_url": "https://chat.bstein.dev/healthz",
|
|
|
|
|
}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def files(tmp_path: Path, content: bytes | None = None) -> dict[str, str]:
|
|
|
|
|
"""Create strict key/policy projections and return their environment."""
|
|
|
|
|
key = tmp_path / "evidence.key"
|
|
|
|
|
key.write_bytes((KEY + "\n").encode() if content is None else content)
|
|
|
|
|
key.chmod(0o400)
|
|
|
|
|
config = tmp_path / "policy.json"
|
|
|
|
|
config.write_text(json.dumps(policy()))
|
|
|
|
|
config.chmod(0o444)
|
|
|
|
|
return {
|
|
|
|
|
release_security.KEY_FILE_ENV: str(key),
|
|
|
|
|
release_security.POLICY_FILE_ENV: str(config),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_good_configuration_and_evidence_identity(tmp_path):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
env["HUX_TENANT_SLOT"] = "slot-3"
|
|
|
|
|
assert release_security.configured(env)
|
|
|
|
|
assert release_security.evidence_key(env) == KEY
|
|
|
|
|
assert release_security.workload_policy(env, "hermes-webui")["flux_kustomization"] == "hermes"
|
|
|
|
|
headers = {
|
|
|
|
|
"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef",
|
|
|
|
|
"X-Hux-Surface": "api", "X-Hux-Trust": "evidence", "X-Hux-Relay-Key": KEY,
|
|
|
|
|
}
|
|
|
|
|
assert identity.resolve(headers, env).trust == "evidence"
|
|
|
|
|
with pytest.raises(Unauthorized, match="api surface"):
|
|
|
|
|
identity.resolve({**headers, "X-Hux-Surface": "chat"}, env)
|
|
|
|
|
with pytest.raises(Unauthorized, match="key missing or wrong"):
|
|
|
|
|
identity.resolve(headers, {**env, release_security.KEY_FILE_ENV: "", "HUX_RELEASE_EVIDENCE_KEY": KEY})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("content", [b"short", b"\xff" * 40, b"", b"x" * 4097])
|
|
|
|
|
def test_key_rejects_invalid_contents(tmp_path, content):
|
|
|
|
|
env = files(tmp_path, content)
|
|
|
|
|
with pytest.raises(Invalid):
|
|
|
|
|
release_security.evidence_key(env)
|
|
|
|
|
assert not release_security.configured(env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_key_rejects_missing_loose_and_symlink_files(tmp_path):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
Path(env[release_security.KEY_FILE_ENV]).chmod(0o440)
|
|
|
|
|
with pytest.raises(Invalid, match="permissions"):
|
|
|
|
|
release_security.evidence_key(env)
|
|
|
|
|
env[release_security.KEY_FILE_ENV] = str(tmp_path / "missing")
|
|
|
|
|
with pytest.raises(Invalid, match="unavailable"):
|
|
|
|
|
release_security.evidence_key(env)
|
|
|
|
|
target = tmp_path / "target"
|
|
|
|
|
target.write_text(KEY)
|
|
|
|
|
link = tmp_path / "link"
|
|
|
|
|
link.symlink_to(target)
|
|
|
|
|
env[release_security.KEY_FILE_ENV] = str(link)
|
|
|
|
|
with pytest.raises(Invalid, match="unavailable"):
|
|
|
|
|
release_security.evidence_key(env)
|
|
|
|
|
with pytest.raises(Invalid, match="missing"):
|
|
|
|
|
release_security._regular_file("", {0o400}, 10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_policy_rejects_encoding_json_shape_and_bounds(tmp_path):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
path = Path(env[release_security.POLICY_FILE_ENV])
|
|
|
|
|
for value in (b"\xff", b"{", json.dumps([]).encode(), json.dumps({"schema": "bad"}).encode()):
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_bytes(value)
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid):
|
|
|
|
|
release_security.load_policy(env)
|
|
|
|
|
for age in (True, 59, 86401):
|
|
|
|
|
item = policy()
|
|
|
|
|
item["max_evidence_age_seconds"] = age
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_text(json.dumps(item))
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid, match="age bound"):
|
|
|
|
|
release_security.load_policy(env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("workloads", [None, {}, {str(index): {} for index in range(17)}])
|
|
|
|
|
def test_policy_rejects_workload_collection(tmp_path, workloads):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
item = policy()
|
|
|
|
|
item["workloads"] = workloads
|
|
|
|
|
path = Path(env[release_security.POLICY_FILE_ENV])
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_text(json.dumps(item))
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid, match="workload policy"):
|
|
|
|
|
release_security.load_policy(env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("field,value,message", [
|
|
|
|
|
("review_url_prefix", "https://scm.bstein.dev/pulls", "end with"),
|
|
|
|
|
("review_url_prefix", "http://scm.bstein.dev/pulls/", "HTTPS"),
|
|
|
|
|
("jenkins_job_url", "https://user:pass@jenkins.bstein.dev/job/x", "HTTPS"),
|
|
|
|
|
("health_url", "https://chat.bstein.dev/healthz?token=x", "HTTPS"),
|
|
|
|
|
("image_repository", "registry:5000/Bad", "image repository"),
|
|
|
|
|
("flux_kustomization", "BAD_name", "Flux kustomization"),
|
|
|
|
|
])
|
|
|
|
|
def test_policy_rejects_unsafe_workload_fields(tmp_path, field, value, message):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
item = policy()
|
|
|
|
|
item["workloads"]["hermes-webui"][field] = value
|
|
|
|
|
path = Path(env[release_security.POLICY_FILE_ENV])
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_text(json.dumps(item))
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid, match=message):
|
|
|
|
|
release_security.load_policy(env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_policy_rejects_bad_workload_shape_and_unknown_name(tmp_path):
|
|
|
|
|
env = files(tmp_path)
|
|
|
|
|
item = policy()
|
|
|
|
|
item["workloads"]["hermes-webui"]["extra"] = True
|
|
|
|
|
path = Path(env[release_security.POLICY_FILE_ENV])
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_text(json.dumps(item))
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid, match="shape"):
|
|
|
|
|
release_security.load_policy(env)
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
path.write_text(json.dumps(policy()))
|
|
|
|
|
path.chmod(0o444)
|
|
|
|
|
with pytest.raises(Invalid, match="not enabled"):
|
|
|
|
|
release_security.workload_policy(env, "hermes-agent")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_trust_gates_are_exact():
|
|
|
|
|
release_security.require_review_creator("router")
|
|
|
|
|
release_security.require_evidence_producer("evidence")
|
|
|
|
|
for trust in ("relay", "worker", "evidence"):
|
|
|
|
|
if trust != "router":
|
|
|
|
|
with pytest.raises(Forbidden, match="router trust"):
|
|
|
|
|
release_security.require_review_creator(trust)
|
|
|
|
|
for trust in ("router", "relay", "worker"):
|
|
|
|
|
with pytest.raises(Forbidden, match="evidence trust"):
|
|
|
|
|
release_security.require_evidence_producer(trust)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_source_is_bounded():
|
|
|
|
|
assert len((FOUNDATION / "hux" / "release_security.py").read_text().splitlines()) <= 500
|