286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""RBAC, Flux, and human-review contracts for the Hermes SCM broker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from testing.tests.test_hermes_scm_broker_support import ROOT, _load_path
|
|
|
|
def _can_i(role: dict, api_group: str, resource: str, verb: str) -> bool:
|
|
return any(
|
|
(api_group in rule["apiGroups"] or "*" in rule["apiGroups"])
|
|
and (resource in rule["resources"] or "*" in rule["resources"])
|
|
and (verb in rule["verbs"] or "*" in rule["verbs"])
|
|
for rule in role["rules"]
|
|
)
|
|
|
|
|
|
def test_agent_rbac_preserves_read_diagnostics_without_administrator_authority():
|
|
documents = list(
|
|
yaml.safe_load_all(
|
|
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
)
|
|
cluster_role, namespaced_role, binding = documents
|
|
assert binding["roleRef"]["name"] == "hermes-agent-cluster-observer-v2"
|
|
assert _can_i(cluster_role, "", "nodes", "get")
|
|
assert _can_i(namespaced_role, "", "pods", "get")
|
|
assert _can_i(namespaced_role, "", "pods/log", "get")
|
|
assert _can_i(
|
|
namespaced_role,
|
|
"kustomize.toolkit.fluxcd.io",
|
|
"kustomizations",
|
|
"list",
|
|
)
|
|
|
|
denied = (
|
|
("", "secrets", "get"),
|
|
("", "serviceaccounts/token", "create"),
|
|
("", "pods", "create"),
|
|
("", "pods/exec", "create"),
|
|
("", "pods/attach", "create"),
|
|
("", "pods/portforward", "create"),
|
|
("rbac.authorization.k8s.io", "clusterrolebindings", "create"),
|
|
("authorization.k8s.io", "selfsubjectaccessreviews", "create"),
|
|
("apps", "deployments", "patch"),
|
|
)
|
|
assert all(
|
|
not _can_i(role, *request)
|
|
for request in denied
|
|
for role in (cluster_role, namespaced_role)
|
|
)
|
|
|
|
bindings = yaml.safe_load(
|
|
(ROOT / "services/hermes-observer-bindings/rolebindings.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)["items"]
|
|
namespaces = {item["metadata"]["namespace"] for item in bindings}
|
|
assert "hermes-scm" not in namespaces
|
|
assert {"cassandra", "flux-system", "hermes", "kube-system"} <= namespaces
|
|
assert all(item["roleRef"]["name"].endswith("namespaced-observer-v2") for item in bindings)
|
|
|
|
|
|
def test_flux_boundary_keeps_broker_secret_and_network_separate_from_agent():
|
|
agent = yaml.safe_load(
|
|
(ROOT / "services/hermes/agent-deployment.yaml").read_text(encoding="utf-8")
|
|
)
|
|
annotations = agent["spec"]["template"]["metadata"]["annotations"]
|
|
assert not any("gitea" in key.lower() for key in annotations)
|
|
assert "GIT_ASKPASS" not in json.dumps(agent)
|
|
|
|
broker = yaml.safe_load(
|
|
(ROOT / "services/hermes-scm-broker/deployment.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
assert broker["metadata"]["namespace"] == "hermes-scm"
|
|
assert broker["spec"]["template"]["spec"]["serviceAccountName"] == (
|
|
"hermes-scm-broker"
|
|
)
|
|
assert "gitea-token" in json.dumps(broker)
|
|
container = broker["spec"]["template"]["spec"]["containers"][0]
|
|
assert container["resources"]["limits"]["memory"] == "768Mi"
|
|
assert container["livenessProbe"]["failureThreshold"] == 10
|
|
tmp = next(
|
|
item for item in broker["spec"]["template"]["spec"]["volumes"] if item["name"] == "tmp"
|
|
)
|
|
assert tmp["emptyDir"]["sizeLimit"] == "1Gi"
|
|
|
|
policy = yaml.safe_load(
|
|
(ROOT / "services/hermes-scm-broker/networkpolicy.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
ingress = policy["spec"]["ingress"]
|
|
assert ingress[0]["from"][0]["namespaceSelector"]["matchLabels"] == {
|
|
"kubernetes.io/metadata.name": "hermes"
|
|
}
|
|
assert ingress[0]["from"][0]["podSelector"]["matchLabels"] == {
|
|
"app": "hermes-agent"
|
|
}
|
|
|
|
vault = (
|
|
ROOT / "services/vault/scripts/vault_k8s_auth_configure.sh"
|
|
).read_text(encoding="utf-8")
|
|
assert 'write_policy_and_role "hermes-scm-broker" "hermes-scm"' in vault
|
|
assert '"hermes/developer-gitea hermes/scm-task-grant" ""' in vault
|
|
agent_start = vault.index('write_policy_and_role "hermes-agent"')
|
|
agent_end = vault.index("write_policy_and_role", agent_start + 1)
|
|
assert "developer-gitea" not in vault[agent_start:agent_end]
|
|
|
|
|
|
def test_flux_bootstrap_has_no_agent_namespace_dependency_cycle():
|
|
hermes_resources = yaml.safe_load(
|
|
(ROOT / "services/hermes/kustomization.yaml").read_text(encoding="utf-8")
|
|
)["resources"]
|
|
assert "namespace.yaml" in hermes_resources
|
|
assert "scm-common" in hermes_resources
|
|
|
|
observer = yaml.safe_load(
|
|
(
|
|
ROOT
|
|
/ "clusters/atlas/flux-system/applications/hermes-observer-rbac/kustomization.yaml"
|
|
).read_text(encoding="utf-8")
|
|
)
|
|
assert "dependsOn" not in observer["spec"]
|
|
observer_objects = [
|
|
item
|
|
for item in yaml.safe_load_all(
|
|
(ROOT / "services/hermes-observer-rbac/rbac.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
if item
|
|
]
|
|
assert {item["kind"] for item in observer_objects} <= {
|
|
"ClusterRole",
|
|
"ClusterRoleBinding",
|
|
}
|
|
|
|
hermes_flux = yaml.safe_load(
|
|
(
|
|
ROOT / "clusters/atlas/flux-system/applications/hermes/kustomization.yaml"
|
|
).read_text(encoding="utf-8")
|
|
)
|
|
assert "hermes-observer-rbac" in {
|
|
item["name"] for item in hermes_flux["spec"]["dependsOn"]
|
|
}
|
|
bindings_flux = yaml.safe_load(
|
|
(
|
|
ROOT
|
|
/ "clusters/atlas/flux-system/applications/hermes-observer-bindings/kustomization.yaml"
|
|
).read_text(encoding="utf-8")
|
|
)
|
|
assert {item["name"] for item in bindings_flux["spec"]["dependsOn"]} == {
|
|
"hermes",
|
|
"hermes-observer-rbac",
|
|
}
|
|
assert bindings_flux["metadata"]["name"] not in {
|
|
item["name"] for item in hermes_flux["spec"]["dependsOn"]
|
|
}
|
|
|
|
applications = (
|
|
ROOT / "clusters/atlas/flux-system/applications/kustomization.yaml"
|
|
).read_text(encoding="utf-8")
|
|
assert "hermes-scm-agent-code" not in applications
|
|
|
|
broker_code = yaml.safe_load(
|
|
(
|
|
ROOT
|
|
/ "clusters/atlas/flux-system/applications/hermes-scm-broker-code/kustomization.yaml"
|
|
).read_text(encoding="utf-8")
|
|
)
|
|
assert broker_code["spec"]["path"] == "./services/hermes/scm-common"
|
|
assert broker_code["spec"]["targetNamespace"] == "hermes-scm"
|
|
assert broker_code["spec"]["dependsOn"] == [{"name": "hermes-scm-namespace"}]
|
|
|
|
|
|
def test_gitea_bootstrap_enforces_human_review_without_overwriting_drift():
|
|
script = (
|
|
ROOT / "services/gitea/scripts/gitea_atlas_identity_ensure.sh"
|
|
).read_text(encoding="utf-8")
|
|
|
|
assert 'required_approvals\\\":1' in script
|
|
assert 'enable_push_whitelist\\\":true' in script
|
|
assert 'push_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
|
assert 'enable_merge_whitelist\\\":true' in script
|
|
assert 'merge_whitelist_usernames\\\":[\\\"${protected_reviewer}\\\"]' in script
|
|
assert "protection differs from the human-review policy" in script
|
|
assert "api_request PATCH" not in script
|
|
job = yaml.safe_load(
|
|
(ROOT / "services/gitea/atlas-identity-bootstrap-job.yaml").read_text()
|
|
)
|
|
init = job["spec"]["template"]["spec"]["initContainers"][0]
|
|
assert init["image"] == (
|
|
"python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
|
|
)
|
|
assert init["securityContext"]["runAsNonRoot"] is True
|
|
assert init["securityContext"]["capabilities"]["drop"] == ["ALL"]
|
|
assert "/opt/python/bin/python3" in script
|
|
|
|
|
|
def _protection(rule_name: str, priority: int, helper, **overrides):
|
|
value = {
|
|
"rule_name": rule_name,
|
|
"priority": priority,
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
**helper._required("bstein"),
|
|
}
|
|
value.update(overrides)
|
|
return value
|
|
|
|
|
|
def test_branch_protection_requires_exact_literal_rules_only():
|
|
helper = _load_path(
|
|
"branch_protection_test",
|
|
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
|
)
|
|
exact = _protection("main", 2, helper)
|
|
assert (
|
|
helper.evaluate(json.dumps([exact]).encode(), "main", "bstein") == "PRESENT"
|
|
)
|
|
|
|
drifted = _protection("main", 2, helper, required_approvals=0)
|
|
with pytest.raises(helper.PolicyError, match="effective main protection differs"):
|
|
helper.evaluate(json.dumps([drifted]).encode(), "main", "bstein")
|
|
|
|
duplicate = _protection("main", 1, helper)
|
|
with pytest.raises(helper.PolicyError, match="duplicate primary-branch"):
|
|
helper.evaluate(json.dumps([exact, duplicate]).encode(), "main", "bstein")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rule_name",
|
|
[
|
|
"release/*",
|
|
"release/**/v1.17",
|
|
"*",
|
|
"**",
|
|
"M*",
|
|
"m{ain,aster}",
|
|
r"m\ain",
|
|
"m?in",
|
|
"m[ai]in",
|
|
],
|
|
)
|
|
def test_branch_glob_rules_fail_closed_pending_human_review(rule_name: str):
|
|
helper = _load_path(
|
|
"branch_protection_glob_parity_test",
|
|
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
|
)
|
|
rules = [_protection(rule_name, 1, helper)]
|
|
with pytest.raises(helper.PolicyError, match="human review"):
|
|
helper.evaluate(json.dumps(rules).encode(), "main", "bstein")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rules",
|
|
[
|
|
[{"rule_name": "main", "priority": 0, "created_at": "2026-01-01T00:00:00Z"}],
|
|
[{"rule_name": "main", "priority": 1, "created_at": "not-a-time"}],
|
|
[{"rule_name": "main"}],
|
|
],
|
|
)
|
|
def test_branch_protection_rejects_ambiguous_rules_before_creation(rules):
|
|
helper = _load_path(
|
|
"branch_protection_adversarial_test",
|
|
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
|
)
|
|
with pytest.raises(helper.PolicyError):
|
|
helper.evaluate(json.dumps(rules).encode(), "main", "bstein")
|
|
|
|
|
|
def test_branch_protection_reports_absent_only_when_no_rule_matches():
|
|
helper = _load_path(
|
|
"branch_protection_absent_test",
|
|
ROOT / "services/gitea/scripts/gitea_branch_protection_check.py",
|
|
)
|
|
rules = [_protection("develop", 1, helper)]
|
|
assert helper.evaluate(json.dumps(rules).encode(), "master", "bstein") == "ABSENT"
|