diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f691485..91c7885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,9 @@ jobs: print(f' {len(p[\"tagOwners\"])} tag owners') " + - name: Policy contract tests + run: python3 -m unittest discover -s tests -p 'test_*.py' + - name: Upload policy artifact uses: actions/upload-artifact@v4 with: diff --git a/constants.dhall b/constants.dhall index 9b87742..d6e9ade 100644 --- a/constants.dhall +++ b/constants.dhall @@ -26,6 +26,7 @@ let tag = , services = "tag:services" , k8s = "tag:k8s" , k8s_operator = "tag:k8s-operator" + , mcp_proxy = "tag:mcp-proxy" , tsidp = "tag:tsidp" , dev = "tag:dev" , staging = "tag:staging" diff --git a/fragments/core.dhall b/fragments/core.dhall index a87a3e8..744fe36 100644 --- a/fragments/core.dhall +++ b/fragments/core.dhall @@ -33,6 +33,10 @@ let tagOwners , mapValue = [ C.tag.k8s_operator, C.autogroup.admin, C.group.dollhouse_admins ] } + , { mapKey = C.tag.mcp_proxy + , mapValue = + [ C.tag.k8s_operator, C.autogroup.admin, C.group.dollhouse_admins ] + } , { mapKey = C.tag.tsidp , mapValue = [ C.autogroup.admin, C.group.dollhouse_admins ] } diff --git a/grants.json b/grants.json index fa4a0f2..1ae674c 100644 --- a/grants.json +++ b/grants.json @@ -31,6 +31,10 @@ "app": {"tailscale.com/cap/secrets": [ {"action": ["get", "info", "put", "activate", "delete"], "secret": ["*"]} ]} +}, { + "src": ["tinyland-honey"], + "dst": ["tag:mcp-proxy"], + "ip": ["tcp:8080"] }, { "src": ["tag:dollhouse"], "dst": ["tag:dollhouse"], diff --git a/tests/test_policy_contract.py b/tests/test_policy_contract.py new file mode 100644 index 0000000..a96d273 --- /dev/null +++ b/tests/test_policy_contract.py @@ -0,0 +1,36 @@ +import json +import unittest +from pathlib import Path + + +POLICY = Path(__file__).resolve().parents[1] / "generated" / "policy.json" + + +class PolicyContractTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.policy = json.loads(POLICY.read_text(encoding="utf-8")) + + def test_kubernetes_operator_owns_mcp_proxy_tag(self) -> None: + owners = self.policy["tagOwners"]["tag:mcp-proxy"] + self.assertIn("tag:k8s-operator", owners) + + def test_honey_mcp_access_is_tcp_only_and_tag_scoped(self) -> None: + expected = { + "src": ["tinyland-honey"], + "dst": ["tag:mcp-proxy"], + "ip": ["tcp:8080"], + } + self.assertEqual(self.policy["grants"].count(expected), 1) + + def test_honey_does_not_receive_broad_kubernetes_acl_access(self) -> None: + broad_rule = { + "action": "accept", + "src": ["tinyland-honey"], + "dst": ["tag:k8s:8080"], + } + self.assertNotIn(broad_rule, self.policy["acls"]) + + +if __name__ == "__main__": + unittest.main()