atlas-iac/testing/tests/test_hermes_scm_task_grants.py

303 lines
12 KiB
Python

"""Task grant, ledger, and broker fast-forward contracts."""
from __future__ import annotations
import sys
import hashlib
import io
import json
import time
from email.message import Message
from pathlib import Path
import pytest
from testing.tests.test_hermes_scm_broker_support import _load, _object_entry, _pack_of, _receive_command
ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(ROOT / "services/hermes/scm-common/scripts"))
import receive_pack_scan # noqa: E402
import scm_task_grants as grants # noqa: E402
KEY = b"a" * 64
def claims(**overrides):
value = {
"repo": "atlas-iac", "ref": "wt/t_root", "base": "main", "board": "titan-iac",
"root_task_id": "t_root", "assignment_task_id": "t_child", "run": "run_1",
"ordinal": 1, "expires": 101, "expected_old": "0" * 40, "new_head": "1" * 40,
"continuation_kind": "",
}
value.update(overrides)
return value
def test_grant_is_exact_signed_schema_and_expiry_bound():
grants = _load("scm_task_grants")
token = grants.sign_grant(claims(), KEY)
assert grants.verify_grant(token, key=KEY, now=100)["root_task_id"] == "t_root"
with pytest.raises(grants.PolicyError, match="signature"):
grants.verify_grant(token[:-1] + "A", key=KEY, now=100)
with pytest.raises(grants.PolicyError, match="expired"):
grants.verify_grant(token, key=KEY, now=101)
def test_ledger_requires_absent_initial_ref_and_exact_owner_cas(tmp_path):
grants = _load("scm_task_grants")
ledger = grants.TaskLedger(tmp_path / "ledger.db")
first = claims()
ledger.register(first, remote_head=None)
ledger.authorize_update(first)
ledger.commit(first)
assert ledger.get("atlas-iac", "wt/t_root") == ("titan-iac", "t_root", "1" * 40)
with pytest.raises(grants.PolicyError, match="head changed"):
ledger.authorize_update(first)
with pytest.raises(grants.PolicyError, match="another task"):
ledger.register(claims(root_task_id="t_other"), remote_head="1" * 40)
with pytest.raises(grants.PolicyError, match="requires operator adoption"):
ledger.register(claims(root_task_id="t_untracked", ref="wt/untracked"), remote_head="2" * 40)
with pytest.raises(grants.PolicyError, match="different branch"):
ledger.register(claims(ref="wt/fork"), remote_head=None)
def test_operator_adoption_seeds_only_the_exact_live_reviewed_branch(tmp_path):
grants = _load("scm_task_grants")
ledger = grants.TaskLedger(tmp_path / "ledger.db")
record = {
"repo": "atlas-iac", "ref": "wt/t_root", "board": "titan-iac",
"root_task_id": "t_root", "latest_head": "a" * 40, "pr_number": 55,
}
ledger.seed_adoption(record, "a" * 40)
assert ledger.get("atlas-iac", "wt/t_root") == ("titan-iac", "t_root", "a" * 40)
with pytest.raises(grants.PolicyError, match="does not match"):
ledger.seed_adoption({**record, "ref": "wt/other"}, "b" * 40)
# A later broker-confirmed revision survives a restart with the original
# reviewed import record still mounted.
ledger.commit(claims(expected_old="a" * 40, new_head="b" * 40))
ledger.seed_adoption(record, "b" * 40)
assert ledger.get("atlas-iac", "wt/t_root")[2] == "b" * 40
def test_fast_forward_requires_quarantined_parent_path():
old, middle, new = "a" * 40, "b" * 40, "c" * 40
assert receive_pack_scan._proves_descends(new, old, {new: (middle,), middle: (old,)})
assert not receive_pack_scan._proves_descends(new, old, {new: (middle,)})
assert not receive_pack_scan._proves_descends(new, old, {new: ("d" * 40,)})
def test_signed_default_wt_branch_accepts_a_real_commit_pack_but_unsigned_does_not():
raw = b"tree " + b"0" * 40 + b"\nauthor test <test@example> 1 +0000\ncommitter test <test@example> 1 +0000\n\nmessage\n"
head = hashlib.sha1(b"commit " + str(len(raw)).encode() + b"\0" + raw).hexdigest()
body = _receive_command(b"0" * 40, head.encode(), b"refs/heads/wt/t_default", _pack_of([_object_entry(1, raw)]))
assert receive_pack_scan.validate_receive_pack(body, "token", (b"token",), expected=("0" * 40, head, "wt/t_default")) == ("0" * 40, head, "wt/t_default")
with pytest.raises(receive_pack_scan.PolicyError, match="namespaced"):
receive_pack_scan.validate_receive_pack(body, "token", (b"token",))
@pytest.mark.parametrize("ref", ["main", "master", "refs/tags/v1"])
def test_granted_push_never_allows_base_or_tag_refs(ref):
if ref.startswith("refs/"):
raw_ref = ref.encode()
else:
raw_ref = f"refs/heads/{ref}".encode()
body = _receive_command(b"0" * 40, b"1" * 40, raw_ref)
with pytest.raises(receive_pack_scan.PolicyError):
receive_pack_scan.validate_receive_pack(body, "token", (b"token",), expected=("0" * 40, "1" * 40, ref))
def test_alias_requests_canonicalize_before_the_broker_forwards_them():
api = _load("gitea_api")
assert api.api_url(api.CANONICAL_BASE_URL, "/api/v1/repos/titan/titan-iac/pulls") == "https://scm.bstein.dev/api/v1/repos/titan/atlas-iac/pulls"
assert api.authorize_request("GET", "/api/v1/repos/titan/atlas-iac/branches/wt/t_root", None) == "branch"
def test_registration_uses_the_fixed_broker_http_envelope():
client = _load("scm_broker_client")
seen = []
class Response:
status = 200
class headers:
@staticmethod
def get_content_type():
return "application/json"
def __enter__(self):
return self
def __exit__(self, *_args):
return False
@staticmethod
def read(_limit):
return b'{"registered":true}'
def opener(request, timeout):
seen.append((request, timeout))
return Response()
assert client.register_task("header-safe-grant", opener=opener) == b'{"registered":true}'
request, timeout = seen[0]
assert request.full_url == client.BROKER_ORIGIN + "/v1/tasks/register"
assert request.data == b'{"grant":"header-safe-grant"}' and timeout == 30
def test_draft_refresh_requires_exact_owned_pr_and_has_no_mutating_fields():
drafts = _load("scm_task_drafts")
grant, number, title, body = drafts.request_fields(
{"grant": "signed", "pr_number": 55, "title": "refresh", "body": "evidence"}, "token"
)
assert (grant, number, title, body) == ("signed", 55, "WIP: refresh", "evidence")
claims = {"repo": "atlas-iac", "ref": "wt/t_root", "base": "main", "new_head": "a" * 40}
pull = {"number": 55, "state": "open", "head": {"ref": "wt/t_root", "sha": "a" * 40, "repo": {"full_name": "titan/atlas-iac"}}, "base": {"ref": "main", "repo": {"full_name": "titan/atlas-iac"}}}
drafts.matches_pull(pull, claims, 55)
with pytest.raises(drafts.PolicyError, match="does not match"):
drafts.matches_pull({**pull, "base": {"ref": "master"}}, claims, 55)
with pytest.raises(drafts.PolicyError, match="fields"):
drafts.request_fields({"grant": "x", "pr_number": 55, "title": "x", "body": "x", "state": "closed"}, "token")
def _draft_update_response(status: int, payload: bytes, content_type: str = "application/json"):
class Response:
def __init__(self):
self.headers = Message()
def __enter__(self):
self.status = status
self.headers["Content-Type"] = content_type
return self
def __exit__(self, *_args):
return False
@staticmethod
def read(_limit):
return payload
return Response()
@pytest.mark.parametrize("status", [200, 201])
def test_draft_update_accepts_only_known_success_statuses_with_matching_pr_shape(
monkeypatch, status
):
drafts = _load("scm_task_drafts")
body = json.dumps({
"number": 55,
"html_url": "https://scm.bstein.dev/titan/atlas-iac/pulls/55",
}).encode()
seen = []
def open_bounded(request, maximum, timeout):
seen.append((request, maximum, timeout))
return _draft_update_response(status, body)
monkeypatch.setattr(drafts.deadline_http, "open_bounded", open_bounded)
assert drafts.update("token", "atlas-iac", 55, "WIP: refreshed", "evidence") == body
request, maximum, timeout = seen[0]
assert request.get_method() == "PATCH"
assert request.full_url.endswith("/api/v1/repos/titan/atlas-iac/pulls/55")
assert json.loads(request.data) == {"title": "WIP: refreshed", "body": "evidence"}
assert maximum == 2 * 1024 * 1024 and timeout == 30
def test_draft_update_rejects_unapproved_success_status(monkeypatch):
drafts = _load("scm_task_drafts")
body = b'{"number":55,"html_url":"https://scm.bstein.dev/titan/atlas-iac/pulls/55"}'
monkeypatch.setattr(
drafts.deadline_http,
"open_bounded",
lambda *_args, **_kwargs: _draft_update_response(202, body),
)
with pytest.raises(drafts.PolicyError, match="upstream response"):
drafts.update("token", "atlas-iac", 55, "WIP: refreshed", "evidence")
@pytest.mark.parametrize(
"payload",
[
b"[]",
b'{"number":56,"html_url":"https://scm.bstein.dev/titan/atlas-iac/pulls/55"}',
b'{"number":55,"html_url":"https://evil.example/titan/atlas-iac/pulls/55"}',
b"{not-json",
],
)
def test_draft_update_rejects_malformed_success_response_shape(monkeypatch, payload):
drafts = _load("scm_task_drafts")
monkeypatch.setattr(
drafts.deadline_http,
"open_bounded",
lambda *_args, **_kwargs: _draft_update_response(201, payload),
)
with pytest.raises(drafts.PolicyError, match="upstream response"):
drafts.update("token", "atlas-iac", 55, "WIP: refreshed", "evidence")
def _control_handler(broker, body: dict):
"""Build the broker's real control handler without opening a TCP listener."""
encoded = json.dumps(body).encode()
handler = object.__new__(broker.BrokerHandler)
headers = Message()
headers["Content-Type"] = "application/json"
headers["Content-Length"] = str(len(encoded))
handler.path = "/v1/tasks/draft-update"
handler.headers = headers
handler.rfile = io.BytesIO(encoded)
handler.wfile = io.BytesIO()
handler.connection = type("Connection", (), {"settimeout": staticmethod(lambda _value: None)})()
handler._json = lambda _status, value: handler.wfile.write(value)
return handler
def test_post_push_repair_refreshes_the_same_owned_pr_after_real_ledger_commit(tmp_path, monkeypatch):
"""Draft prose refresh follows an already committed old->new branch update."""
broker = _load("scm_broker")
ledger = grants.TaskLedger(tmp_path / "ledger.db")
old, new = "a" * 40, "b" * 40
initial = {
**claims(expected_old="0" * 40, new_head=old, expires=int(time.time()) + 120),
"continuation_kind": "implementation",
}
ledger.register(initial, remote_head=None)
ledger.commit(initial)
repair = {
**claims(
assignment_task_id="t_repair", expected_old=old, new_head=new,
expires=int(time.time()) + 120,
),
"continuation_kind": "repair",
}
# The receive-pack path has already performed the authenticated CAS. The
# following refresh must authorize the owner at `new`, not re-check old.
ledger.commit(repair)
token = grants.sign_grant(repair, KEY)
pull = {
"number": 55, "state": "open",
"head": {"ref": "wt/t_root", "sha": new, "repo": {"full_name": "titan/atlas-iac"}},
"base": {"ref": "main", "repo": {"full_name": "titan/atlas-iac"}},
}
updates = []
monkeypatch.setattr(broker, "read_token", lambda: "token")
monkeypatch.setattr(broker, "verify_grant", lambda value: grants.verify_grant(value, key=KEY))
monkeypatch.setattr(broker, "_task_ledger", lambda: ledger)
monkeypatch.setattr(broker, "_branch_head", lambda *_args: new)
monkeypatch.setattr(broker, "read", lambda *_args, **_kwargs: json.dumps(pull).encode())
monkeypatch.setattr(
broker, "update_draft",
lambda _token, repo, number, title, body: updates.append((repo, number, title, body))
or json.dumps({"number": number, "html_url": "https://scm.bstein.dev/titan/atlas-iac/pulls/55"}).encode(),
)
_control_handler(broker, {
"grant": token, "pr_number": 55, "title": "Repair CI failure", "body": "Tests: pytest",
})._control()
assert ledger.get("atlas-iac", "wt/t_root") == ("titan-iac", "t_root", new)
assert updates == [("atlas-iac", 55, "WIP: Repair CI failure", "Tests: pytest")]