atlas-iac/testing/tests/test_hermes_hux_artifact_auth.py

147 lines
8.2 KiB
Python
Raw Permalink Normal View History

"""HUX-04 artifacts: authorization boundaries and tenant isolation.
Security obligations exercised: a second subject sees 404, never 403, for
every artifact route (SO-33, SO-34); malformed ids never reach a path (SO-43);
a forged owner field inside the tenant directory is still refused; every
denial leaves an audit outcome; the card serves nothing while its flag is off.
"""
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 artifacts, audit, contracts, errors, identity, store # noqa: E402
from hux.server import build_router # noqa: E402
SCHEMAS = contracts.load_all()
OWNER = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
OTHER = {**OWNER, "X-Hux-Subject": "usr_fedcba9876543210"}
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
@pytest.fixture
def router(tmp_path):
return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
def call(router, method, path, body=None, headers=OWNER):
payload = json.dumps(body).encode() if body is not None else b""
response = router.dispatch(method, path, headers, payload)
return response.status, response.body
@pytest.fixture
def artifact(router):
status, record = call(router, "POST", "/hux/v1/artifacts", {"type": "markdown", "title": "Mine", "content": "secret\n", "conversation_id": "conv_0001abcd"})
assert status == 201
return record
def test_second_subject_gets_404_everywhere(router, artifact):
art = artifact["id"]
probes = [
("GET", f"/hux/v1/artifacts/{art}", None),
("GET", f"/hux/v1/artifacts/{art}/versions/1", None),
("GET", f"/hux/v1/artifacts/{art}/versions/1/diff", None),
("POST", f"/hux/v1/artifacts/{art}/versions", {"content": "x"}),
("POST", f"/hux/v1/artifacts/{art}/promote", {"project_id": "prj_0001aaaa"}),
]
for method, path, body in probes:
status, error = call(router, method, path, body, {**OTHER, "If-Match": "1"})
assert (status, error["code"]) == (404, "not_found"), path
assert contracts.validate_record(error, SCHEMAS) == []
status, body = call(router, "GET", "/hux/v1/artifacts?conversation_id=conv_0001abcd", headers=OTHER)
assert status == 200 and body["items"] == []
rows = audit.recent(store.TenantStore(router.data_root, identity.Identity("slot-3", "usr_fedcba9876543210", "chat", "router")))
assert [r["outcome"] for r in rows if r["action"].startswith("artifacts.")].count("not_found") == 5
def test_cross_tenant_lineage_forgery_is_404(router, artifact):
body = {"type": "markdown", "title": "Derived", "content": "copy", "lineage": {"artifact_id": artifact["id"], "version": 1}}
status, error = call(router, "POST", "/hux/v1/artifacts", body, OTHER)
assert (status, error["code"]) == (404, "not_found")
status, own = call(router, "POST", "/hux/v1/artifacts", {**body, "lineage": None}, OTHER)
assert status == 201
status, error = call(router, "POST", f"/hux/v1/artifacts/{own['id']}/versions", {"content": "v2", "lineage": {"artifact_id": artifact["id"], "version": 1}}, {**OTHER, "If-Match": "1"})
assert status == 404
assert call(router, "GET", f"/hux/v1/artifacts/{own['id']}", headers=OTHER)[1]["current_version"] == 1
def test_malformed_ids_never_reach_the_filesystem(router):
for path in ("/hux/v1/artifacts/..", "/hux/v1/artifacts/art_x", "/hux/v1/artifacts/ART_0000000000"):
status, error = call(router, "GET", path)
assert status == 404 and error["code"] == "not_found", path
tenant = store.TenantStore(router.data_root, identity.Identity("slot-3", "usr_0123456789abcdef", "chat", "router"))
assert not (tenant.root / "artifacts" / "...json").exists()
def test_forged_owner_inside_tenant_directory_is_refused(router, artifact):
tenant = store.TenantStore(router.data_root, identity.Identity("slot-3", "usr_0123456789abcdef", "chat", "router"))
forged = {**tenant.get("artifacts", artifact["id"]), "id": "art_forged000001", "owner": "usr_fedcba9876543210"}
tenant.put("artifacts", forged)
assert call(router, "GET", "/hux/v1/artifacts/art_forged000001")[0] == 404
status, error = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "t", "content": "c", "lineage": {"artifact_id": "art_forged000001", "version": 1}})
assert status == 404
status, body = call(router, "GET", "/hux/v1/artifacts")
assert [a["id"] for a in body["items"]] == [artifact["id"]]
assert artifacts.artifact_exists(tenant, "art_forged000001")
def test_contract_validation_guards_every_write(router, artifact):
tenant = store.TenantStore(router.data_root, identity.Identity("slot-3", "usr_0123456789abcdef", "chat", "router"))
current = tenant.get("artifacts", artifact["id"])
with pytest.raises(errors.Invalid) as raised:
artifacts.append_version(tenant, current, {"version": 2, "created_at": "now", "created_by": {"type": "user", "id": "x"}, "content_ref": {"hash": "sha256:" + "a" * 64, "bytes": 1, "mime": "x"}}, 1)
assert raised.value.details and tenant.get("artifacts", artifact["id"])["current_version"] == 1
def test_flag_off_hides_the_card(tmp_path, artifact):
router = build_router(tmp_path, {"HUX_FLAGS": "hux.foundation", "HUX_ROUTER_KEY": "rk"})
status, error = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "t", "content": "c"})
assert (status, error["code"]) == (404, "flag_off")
status, error = call(router, "GET", "/hux/v1/artifacts")
assert status == 404
def test_idempotency_replay_is_scoped_to_the_caller(router):
body = {"type": "code", "title": "t", "content": "c"}
status, mine = call(router, "POST", "/hux/v1/artifacts", body, {**OWNER, "Idempotency-Key": "shared-key-01"})
status, theirs = call(router, "POST", "/hux/v1/artifacts", body, {**OTHER, "Idempotency-Key": "shared-key-01"})
assert status == 201 and theirs["id"] != mine["id"] and theirs["owner"] == "usr_fedcba9876543210"
status, again = call(router, "POST", "/hux/v1/artifacts", body, {**OWNER, "Idempotency-Key": "shared-key-02"})
assert status == 201 and again["id"] != mine["id"]
status, error = call(router, "POST", f"/hux/v1/artifacts/{mine['id']}/versions", {"content": "v"}, {**OWNER, "If-Match": "1", "Idempotency-Key": "shared-key-02"})
assert status == 201
def test_audit_rows_name_family_verbs(router, artifact):
call(router, "GET", f"/hux/v1/artifacts/{artifact['id']}")
rows = audit.recent(store.TenantStore(router.data_root, identity.Identity("slot-3", "usr_0123456789abcdef", "chat", "router")))
assert [r["action"] for r in rows] == ["artifacts.create", "artifacts.get"]
assert all(contracts.validate("common.schema.json", r, SCHEMAS, "/$defs/audit_outcome") == [] for r in rows)
def test_secret_bearing_content_is_kept_but_forced_restricted(router):
"""Review a2-C / SO-12: user artifacts are not rewritten, but a secret pattern forces restricted sensitivity and is audited."""
token = "ghp_" + "A" * 36
status, record = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "env", "content": f"TOKEN={token}", "sensitivity": "personal"})
assert status == 201 and record["sensitivity"] == "restricted"
status, version = call(router, "GET", f"/hux/v1/artifacts/{record['id']}/versions/1")
assert status == 200 and token in version["content"]
clean = call(router, "POST", "/hux/v1/artifacts", {"type": "code", "title": "clean", "content": "x = 1"})[1]
assert clean["sensitivity"] == "personal"
status, bumped = call(router, "POST", f"/hux/v1/artifacts/{clean['id']}/versions", {"content": f"key: {token}"}, {**OWNER, "If-Match": "1"})
assert status == 201 and bumped["sensitivity"] == "restricted"
from hux import audit, store
reasons = [r.get("reason") for r in audit.recent(store.TenantStore(router.data_root, identity.resolve(OWNER, {"HUX_ROUTER_KEY": "rk"}))) if r["action"].startswith("artifacts.")]
assert reasons.count("secret_pattern_restricted") == 2