test: forward-direction issue-comment permission separation (#137 follow-up) #140
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user