From 9b84ecf592926601fa7b39a2c2144b3baa9c16a9 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Sun, 5 Jul 2026 02:40:42 -0400 Subject: [PATCH] test: forward-direction issue-comment permission separation (#137 follow-up) PR #138 added the issue.comment alias and example grants. Add the forward-direction separation guards the migration decision requires: gitea.issue.comment never implies issue close, PR review/approve/merge, or branch push / repo commit; gitea.pr.comment never implies gitea.issue.comment; the pre-#137 live author op set still fails closed with the exact operator-facing reason; and the shipped v1/v2 example configs keep granting issue comments while preserving author/reviewer role separation. Refs #137, #139 Co-Authored-By: Claude Fable 5 --- tests/test_mcp_server.py | 96 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index f52b129..2e48a1c 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -35,6 +35,7 @@ from mcp_server import ( # noqa: E402 gitea_create_issue_comment, ) from gitea_auth import get_profile # noqa: E402 +import gitea_config # noqa: E402 FAKE_AUTH = "Basic dGVzdDp0ZXN0" @@ -2186,3 +2187,98 @@ class TestIssueCommentTools(unittest.TestCase): with self.assertRaises(ValueError): gitea_create_issue_comment( issue_number=9, body="x", remote="nope") + + +# --------------------------------------------------------------------------- +# Issue-comment permission separation (#137 follow-up; #139 groundwork) +# --------------------------------------------------------------------------- +class TestIssueCommentPermissionSeparation(unittest.TestCase): + """gitea.issue.comment grants exactly issue-thread commenting. + + #138 added the ``issue.comment`` alias and granted it in the example + configs. These are the forward-direction separation guards: holding the + permission must never imply issue close, PR review/approve/merge, or + branch push / repo commit — and the shipped examples must keep granting + it so the live-config migration gap (#137) cannot silently regress. + """ + + ISSUE_COMMENT_ONLY = ["gitea.issue.comment"] + + def test_issue_comment_does_not_imply_issue_close(self): + ok, reason = gitea_config.check_operation( + "gitea.issue.close", self.ISSUE_COMMENT_ONLY) + self.assertFalse(ok) + self.assertEqual(reason, "not-allowed") + + def test_issue_comment_does_not_imply_review_or_merge(self): + for op in ("gitea.pr.review", "gitea.pr.approve", "gitea.pr.merge", + "gitea.pr.request_changes"): + ok, _ = gitea_config.check_operation(op, self.ISSUE_COMMENT_ONLY) + self.assertFalse(ok, msg=f"{op} must not be implied") + + def test_issue_comment_does_not_imply_authoring_ops(self): + for op in ("gitea.branch.push", "gitea.repo.commit", + "gitea.branch.create", "gitea.pr.create"): + ok, _ = gitea_config.check_operation(op, self.ISSUE_COMMENT_ONLY) + self.assertFalse(ok, msg=f"{op} must not be implied") + + def test_pr_comment_does_not_imply_issue_comment(self): + ok, reason = gitea_config.check_operation( + "gitea.issue.comment", ["gitea.read", "gitea.pr.comment"]) + self.assertFalse(ok) + self.assertEqual(reason, "not-allowed") + + def test_pre_137_author_op_set_fails_closed(self): + # Exactly the live prgs-author op set that blocked the #51 audit + # comment before the #137/#138 migration. + env = {"GITEA_PROFILE_NAME": "gitea-author", + "GITEA_ALLOWED_OPERATIONS": + "gitea.branch.create,gitea.branch.push,gitea.pr.comment," + "gitea.pr.create,gitea.read,gitea.repo.commit"} + with patch.dict(os.environ, env, clear=True): + result = gitea_create_issue_comment( + issue_number=51, body="audit findings", remote="prgs") + self.assertFalse(result["success"]) + self.assertIn("profile is not allowed to gitea.issue.comment", + result["reasons"]) + + # -- shipped examples must keep granting the op -------------------------- + + @staticmethod + def _example(name): + path = os.path.join(os.path.dirname(__file__), "..", name) + with open(path, encoding="utf-8") as fh: + return json.load(fh) + + def test_v2_example_profiles_grant_issue_comment(self): + cfg = self._example("gitea-mcp.v2-contexts.example.json") + for name, profile in cfg["profiles"].items(): + allowed = profile.get("allowed_operations", []) + ok, _ = gitea_config.check_operation( + "gitea.issue.comment", allowed, + profile.get("forbidden_operations", [])) + self.assertTrue( + ok, msg=f"example profile {name} must grant issue comments") + + def test_v2_example_preserves_role_separation(self): + cfg = self._example("gitea-mcp.v2-contexts.example.json") + author = cfg["profiles"]["example-author"] + reviewer = cfg["profiles"]["example-reviewer"] + for op in ("gitea.pr.approve", "gitea.pr.merge"): + ok, _ = gitea_config.check_operation( + op, author["allowed_operations"], + author.get("forbidden_operations", [])) + self.assertFalse(ok, msg=f"author example must not allow {op}") + for op in ("gitea.branch.push", "gitea.repo.commit"): + ok, _ = gitea_config.check_operation( + op, reviewer["allowed_operations"], + reviewer.get("forbidden_operations", [])) + self.assertFalse(ok, msg=f"reviewer example must not allow {op}") + + def test_v1_example_reviewer_grants_issue_comment(self): + cfg = self._example("gitea-mcp.example.json") + reviewer = cfg["profiles"]["mdcps-reviewer"] + ok, _ = gitea_config.check_operation( + "gitea.issue.comment", reviewer["allowed_operations"], + reviewer.get("forbidden_operations", [])) + self.assertTrue(ok)