fix: enforce merger role boundaries (Closes #539)
This commit is contained in:
+63
-6
@@ -211,6 +211,21 @@ def _effective_workspace_role() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _profile_role_kind(profile: dict) -> str:
|
||||
"""Resolve a profile's declared role before inferring from permissions."""
|
||||
role = (profile.get("role") or profile.get("role_kind") or "").strip()
|
||||
if role:
|
||||
return role
|
||||
profile_name = (profile.get("profile_name") or "").strip().lower()
|
||||
for candidate in ("reconciler", "merger", "reviewer", "author"):
|
||||
if candidate in profile_name:
|
||||
return candidate
|
||||
return _role_kind(
|
||||
profile.get("allowed_operations") or [],
|
||||
profile.get("forbidden_operations") or [],
|
||||
)
|
||||
|
||||
|
||||
def _actual_profile_role() -> str:
|
||||
"""Resolve the workspace role from the ACTIVE PROFILE alone (#540).
|
||||
|
||||
@@ -223,10 +238,7 @@ def _actual_profile_role() -> str:
|
||||
``author`` here, so author blocking is preserved.
|
||||
"""
|
||||
profile = get_profile()
|
||||
role = _role_kind(
|
||||
profile.get("allowed_operations") or [],
|
||||
profile.get("forbidden_operations") or [],
|
||||
)
|
||||
role = _profile_role_kind(profile)
|
||||
return nwb.normalize_role_kind(
|
||||
role,
|
||||
profile_name=profile.get("profile_name"),
|
||||
@@ -9917,6 +9929,24 @@ def gitea_resolve_task_capability(
|
||||
|
||||
required_permission = task_capability_map.required_permission(task)
|
||||
required_role = task_capability_map.required_role(task)
|
||||
role_exclusive_tasks = {
|
||||
"review_pr",
|
||||
"approve_pr",
|
||||
"request_changes_pr",
|
||||
"blind_pr_queue_review",
|
||||
"pr_queue_cleanup",
|
||||
"pr-queue-cleanup",
|
||||
"merge_pr",
|
||||
"create_branch",
|
||||
"push_branch",
|
||||
"create_pr",
|
||||
"commit_files",
|
||||
"gitea_commit_files",
|
||||
"address_pr_change_requests",
|
||||
"delete_branch",
|
||||
"work_issue",
|
||||
"work-issue",
|
||||
}
|
||||
|
||||
infra_assessment = role_session_router.assess_infra_stop(PROJECT_ROOT)
|
||||
if required_role == "reviewer":
|
||||
@@ -10011,11 +10041,26 @@ def gitea_resolve_task_capability(
|
||||
# Load active permissions
|
||||
active_allowed = profile.get("allowed_operations") or []
|
||||
active_forbidden = profile.get("forbidden_operations") or []
|
||||
active_role_kind = _profile_role_kind(profile)
|
||||
|
||||
# Check if allowed in current session
|
||||
allowed_in_current_session, _ = gitea_config.check_operation(
|
||||
permission_allowed_in_current_session, _ = gitea_config.check_operation(
|
||||
required_permission, active_allowed, active_forbidden
|
||||
)
|
||||
role_matches_current_session = (
|
||||
task not in role_exclusive_tasks
|
||||
or active_role_kind == required_role
|
||||
)
|
||||
role_mismatch_reason = None
|
||||
if not role_matches_current_session:
|
||||
role_mismatch_reason = (
|
||||
f"Active profile role '{active_role_kind}' cannot perform "
|
||||
f"{required_role} task '{task}' even if nearby permissions are "
|
||||
"present (fail closed)."
|
||||
)
|
||||
allowed_in_current_session = (
|
||||
permission_allowed_in_current_session and role_matches_current_session
|
||||
)
|
||||
|
||||
switching = gitea_config.is_runtime_switching_enabled()
|
||||
available_in_session = allowed_in_current_session
|
||||
@@ -10042,7 +10087,13 @@ def gitea_resolve_task_capability(
|
||||
except Exception:
|
||||
pass
|
||||
ok, _ = gitea_config.check_operation(required_permission, p_allowed_n, p_forbidden_n)
|
||||
if ok:
|
||||
p_role = (p_data.get("role") or "").strip()
|
||||
p_role_ok = (
|
||||
task not in role_exclusive_tasks
|
||||
or not p_role
|
||||
or p_role == required_role
|
||||
)
|
||||
if ok and p_role_ok:
|
||||
matching_profiles.append(p_name)
|
||||
|
||||
configured = len(matching_profiles) > 0
|
||||
@@ -10070,6 +10121,8 @@ def gitea_resolve_task_capability(
|
||||
reason_msg = (
|
||||
f"No profile configured with permission '{required_permission}'."
|
||||
)
|
||||
elif role_mismatch_reason:
|
||||
reason_msg = role_mismatch_reason
|
||||
different_namespace_required = False
|
||||
next_safe_action = "None; ready for operations."
|
||||
|
||||
@@ -10101,6 +10154,8 @@ def gitea_resolve_task_capability(
|
||||
# into author-side mutations.
|
||||
stop_required = not allowed_in_current_session
|
||||
task_role_guidance = []
|
||||
if role_mismatch_reason:
|
||||
task_role_guidance.append(f"STOP: {role_mismatch_reason}")
|
||||
if required_role == "reviewer":
|
||||
if allowed_in_current_session:
|
||||
task_role_guidance.append(
|
||||
@@ -10146,7 +10201,9 @@ def gitea_resolve_task_capability(
|
||||
"required_role_kind": required_role,
|
||||
"active_profile": profile["profile_name"],
|
||||
"active_identity": username,
|
||||
"active_role_kind": active_role_kind,
|
||||
"active_profile_allowed_operations": active_allowed,
|
||||
"active_profile_permission_allowed": permission_allowed_in_current_session,
|
||||
"allowed_in_current_session": allowed_in_current_session,
|
||||
"available_in_session": available_in_session,
|
||||
"configured": configured,
|
||||
|
||||
Reference in New Issue
Block a user