fix(mcp): remediate break-glass restart authorization and audit findings (#664)
This commit is contained in:
+192
-58
@@ -23923,6 +23923,7 @@ def gitea_request_mcp_restart(
|
||||
|
||||
|
||||
BREAK_GLASS_CONFIRMATION_PHRASE = "I_ACKNOWLEDGE_BREAK_GLASS_MCP_RESTART_DISRUPTION"
|
||||
PRIVILEGED_BREAK_GLASS_ROLES = frozenset({"controller", "operator", "admin", "sysadmin"})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -23943,15 +23944,14 @@ def gitea_break_glass_restart(
|
||||
|
||||
Break-glass restart permits emergency recovery when graceful drain cannot
|
||||
complete. It requires:
|
||||
1. Privileged caller authorization (ordinary LLM author/reviewer/merger/reconciler
|
||||
roles are denied fail-closed; controller/admin/sysadmin or GITEA_BREAKGLASS_RESTART_AUTHORIZATION
|
||||
is required).
|
||||
1. Privileged caller authorization (explicit allowlist: controller, operator,
|
||||
admin, sysadmin; ordinary roles and unknown/malformed roles fail closed).
|
||||
2. Explicit non-empty reason (minimum 10 characters).
|
||||
3. Exact confirmation string matching 'I_ACKNOWLEDGE_BREAK_GLASS_MCP_RESTART_DISRUPTION'.
|
||||
4. Mandatory impact acknowledgement (impact_ack=True).
|
||||
5. Immutable audit entry recorded.
|
||||
6. Automatic incident record created on Gitea.
|
||||
7. Mandatory post-restart reconciliation requirement (#662).
|
||||
5. Immutable append-only audit entry recorded prior to execution.
|
||||
6. Automatic incident record created on Gitea prior to execution.
|
||||
7. Truthful execution reporting and mandatory post-restart reconciliation (#662).
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
@@ -23971,24 +23971,24 @@ def gitea_break_glass_restart(
|
||||
(os.environ.get("GITEA_BREAKGLASS_RESTART_AUTHORIZATION") or "").strip()
|
||||
)
|
||||
|
||||
# AC1: Ordinary LLM roles (author, reviewer, merger, reconciler) cannot invoke break-glass
|
||||
# unless explicit environment break-glass authorization is configured.
|
||||
if active_role in ("author", "reviewer", "merger", "reconciler") and not break_glass_env_auth:
|
||||
# 1. Privileged role authorization (explicit allowlist - Finding 1 & 5)
|
||||
if active_role not in PRIVILEGED_BREAK_GLASS_ROLES:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"active_role": active_role,
|
||||
"reasons": [
|
||||
f"ordinary LLM role '{active_role}' is forbidden from break-glass restarts (#664 AC1); "
|
||||
"privileged controller, operator, or GITEA_BREAKGLASS_RESTART_AUTHORIZATION required"
|
||||
f"role '{active_role}' is not in privileged break-glass allowlist "
|
||||
f"({sorted(PRIVILEGED_BREAK_GLASS_ROLES)}); break-glass restart requires a privileged role (#664 AC1)"
|
||||
],
|
||||
"blocker_kind": "role_authorization",
|
||||
}
|
||||
|
||||
# AC2: Required fields enforced
|
||||
clean_reason = (reason or "").strip()
|
||||
if not clean_reason or len(clean_reason) < 10:
|
||||
# 2. Required fields and redaction (Finding 3)
|
||||
raw_reason = (reason or "").strip()
|
||||
clean_reason = _redact(raw_reason)
|
||||
if not raw_reason or len(raw_reason) < 10:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
@@ -24022,6 +24022,19 @@ def gitea_break_glass_restart(
|
||||
"blocker_kind": "impact_ack_required",
|
||||
}
|
||||
|
||||
# Gate incident issue creation on gitea.issue.create permission (Finding 3, B3)
|
||||
if create_incident_issue:
|
||||
create_block = _profile_operation_gate("gitea.issue.create")
|
||||
if create_block:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"reasons": create_block,
|
||||
"permission_report": _permission_block_report("gitea.issue.create"),
|
||||
"blocker_kind": "permission_denied",
|
||||
}
|
||||
|
||||
# Evaluate impact / disrupted sessions
|
||||
impact_result = gitea_request_mcp_restart(
|
||||
remote=remote,
|
||||
@@ -24047,58 +24060,179 @@ def gitea_break_glass_restart(
|
||||
"confirmation": clean_confirmation,
|
||||
"restart_class": restart_class,
|
||||
"disrupted_sessions_count": disrupted_count,
|
||||
"disrupted_sessions": [s.get("session_id") if isinstance(s, dict) else str(s) for s in disrupted_sessions],
|
||||
"disrupted_sessions": [
|
||||
s.get("session_id") if isinstance(s, dict) else str(s)
|
||||
for s in disrupted_sessions
|
||||
],
|
||||
"dry_run": dry_run,
|
||||
"remote": remote,
|
||||
"org": o,
|
||||
"repo": r,
|
||||
"env_auth_present": break_glass_env_auth,
|
||||
}
|
||||
|
||||
# Save immutable audit record
|
||||
saved_audit = mcp_session_state.save_state(
|
||||
kind="break_glass_audit",
|
||||
payload=audit_payload,
|
||||
# 4. Dry-run handling (Finding 4, B7)
|
||||
if dry_run:
|
||||
return {
|
||||
"success": True,
|
||||
"performed": False,
|
||||
"dry_run": True,
|
||||
"break_glass_executed": False,
|
||||
"would_execute": True,
|
||||
"actor": identity,
|
||||
"role": active_role,
|
||||
"restart_class": restart_class,
|
||||
"reason": clean_reason,
|
||||
"confirmation": clean_confirmation,
|
||||
"disrupted_sessions_count": disrupted_count,
|
||||
"disrupted_sessions": disrupted_sessions,
|
||||
"audit_record": audit_payload,
|
||||
"saved_audit": None,
|
||||
"incident_issue": None,
|
||||
"reconciliation_required": True,
|
||||
"reconciliation_tool": "gitea_reconcile_after_restart",
|
||||
"follow_up_issue_required": True,
|
||||
"cross_references": ["#652", "#653", "#655", "#630", "#658", "#662", "#664"],
|
||||
"reasons": ["break-glass restart dry-run evaluated successfully"],
|
||||
}
|
||||
|
||||
# Fail closed if create_incident_issue is False on real execution (Finding 2, B5)
|
||||
if not create_incident_issue:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"reasons": [
|
||||
"create_incident_issue=False is forbidden on real break-glass execution; "
|
||||
"pre-execution incident creation is mandatory (#664 AC3)"
|
||||
],
|
||||
"blocker_kind": "incident_creation_required",
|
||||
}
|
||||
|
||||
# Pre-execution recording: Audit record (Finding 2, B4)
|
||||
audit_event = gitea_audit.build_event(
|
||||
action="break_glass_mcp_restart",
|
||||
result=gitea_audit.SUCCEEDED,
|
||||
remote=remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
profile_identity=profile.get("profile_name", "unknown"),
|
||||
server=(gitea_url(h, "").rstrip("/") if h else None),
|
||||
repository=r,
|
||||
profile_name=profile.get("profile_name", "unknown"),
|
||||
audit_label=profile.get("audit_label", "unknown"),
|
||||
authenticated_username=identity,
|
||||
reason=clean_reason,
|
||||
mcp_namespace="gitea-author",
|
||||
task_role=active_role,
|
||||
operation="break_glass_mcp_restart",
|
||||
now=now_iso,
|
||||
request_metadata={
|
||||
"confirmation": clean_confirmation,
|
||||
"restart_class": restart_class,
|
||||
"disrupted_sessions_count": disrupted_count,
|
||||
"disrupted_sessions": [
|
||||
s.get("session_id") if isinstance(s, dict) else str(s)
|
||||
for s in disrupted_sessions
|
||||
],
|
||||
},
|
||||
)
|
||||
audit_write_success = True
|
||||
if gitea_audit.audit_enabled():
|
||||
audit_write_success = gitea_audit.write_event(audit_event)
|
||||
|
||||
if not audit_write_success:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"reasons": [
|
||||
"failed to persist required pre-execution audit event (#664 AC3)"
|
||||
],
|
||||
"blocker_kind": "audit_recording_failed",
|
||||
}
|
||||
|
||||
# Pre-execution recording: Gitea Incident Issue (Finding 2 & 3, B5)
|
||||
issue_title = f"[INCIDENT] Break-glass MCP restart invoked by {identity}"
|
||||
issue_body = (
|
||||
f"## Break-glass MCP restart incident report (#664)\n\n"
|
||||
f"- **Invoked by**: `{identity}` (role: `{active_role}`)\n"
|
||||
f"- **Timestamp**: `{now_iso}`\n"
|
||||
f"- **Reason**: {clean_reason}\n"
|
||||
f"- **Confirmation**: `{clean_confirmation}`\n"
|
||||
f"- **Disrupted Sessions Count**: `{disrupted_count}`\n\n"
|
||||
f"### Mandatory Post-Restart Reconciliation (#662)\n"
|
||||
f"Post-restart reconciliation must be executed via `gitea_reconcile_after_restart` "
|
||||
f"to clean up orphaned leases, inspect worktree integrity, and handle disrupted work.\n\n"
|
||||
f"### Cross-references\n"
|
||||
f"Ref #652 #653 #655 #630 #658 #662 #664\n"
|
||||
)
|
||||
incident_issue_result = None
|
||||
try:
|
||||
incident_issue_result = api_request(
|
||||
"POST",
|
||||
f"{repo_api_url(h, o, r)}/issues",
|
||||
_auth(h),
|
||||
{
|
||||
"title": issue_title,
|
||||
"body": issue_body,
|
||||
"labels": ["incident", "mcp-health", "break-glass"],
|
||||
},
|
||||
)
|
||||
if not isinstance(incident_issue_result, dict) or "number" not in incident_issue_result:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"reasons": [
|
||||
f"incident issue creation failed (#664 AC3): {incident_issue_result}"
|
||||
],
|
||||
"blocker_kind": "incident_creation_failed",
|
||||
"incident_issue": incident_issue_result,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"reasons": [
|
||||
f"incident issue creation failed with exception (#664 AC3): {_redact(str(exc))}"
|
||||
],
|
||||
"blocker_kind": "incident_creation_failed",
|
||||
"incident_issue": {"error": _redact(str(exc))},
|
||||
}
|
||||
|
||||
# Execute or delegate canonical non-dry-run restart (Finding 4)
|
||||
restart_exec_result = gitea_request_mcp_restart(
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
dry_run=False,
|
||||
restart_class=restart_class,
|
||||
request_break_glass=True,
|
||||
)
|
||||
|
||||
incident_issue_result = None
|
||||
if create_incident_issue and not dry_run:
|
||||
issue_title = f"[INCIDENT] Break-glass MCP restart invoked by {identity}"
|
||||
issue_body = (
|
||||
f"## Break-glass MCP restart incident report (#664)\n\n"
|
||||
f"- **Invoked by**: `{identity}` (role: `{active_role}`)\n"
|
||||
f"- **Timestamp**: `{now_iso}`\n"
|
||||
f"- **Reason**: {clean_reason}\n"
|
||||
f"- **Confirmation**: `{clean_confirmation}`\n"
|
||||
f"- **Disrupted Sessions Count**: `{disrupted_count}`\n\n"
|
||||
f"### Mandatory Post-Restart Reconciliation (#662)\n"
|
||||
f"Post-restart reconciliation must be executed via `gitea_reconcile_after_restart` "
|
||||
f"to clean up orphaned leases, inspect worktree integrity, and handle disrupted work.\n\n"
|
||||
f"### Cross-references\n"
|
||||
f"Ref #652 #653 #655 #630 #658 #662 #664\n"
|
||||
)
|
||||
try:
|
||||
incident_issue_result = api_request(
|
||||
"POST",
|
||||
f"{repo_api_url(h, o, r)}/issues",
|
||||
_auth(h),
|
||||
{
|
||||
"title": issue_title,
|
||||
"body": issue_body,
|
||||
"labels": ["incident", "mcp-health", "break-glass"],
|
||||
},
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
incident_issue_result = {"error": _redact(str(exc))}
|
||||
if not restart_exec_result.get("apply_authorized") or not restart_exec_result.get("success"):
|
||||
return {
|
||||
"success": False,
|
||||
"performed": False,
|
||||
"break_glass_executed": False,
|
||||
"actor": identity,
|
||||
"role": active_role,
|
||||
"restart_class": restart_class,
|
||||
"reasons": [
|
||||
"delegated restart execution failed or was denied by coordinator (#664)",
|
||||
*(restart_exec_result.get("reasons") or []),
|
||||
],
|
||||
"blocker_kind": "restart_delegation_failed",
|
||||
"restart_result": restart_exec_result,
|
||||
"incident_issue": incident_issue_result,
|
||||
"audit_record": audit_event,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"performed": not dry_run,
|
||||
"dry_run": dry_run,
|
||||
"break_glass_executed": not dry_run,
|
||||
"performed": True,
|
||||
"dry_run": False,
|
||||
"break_glass_executed": True,
|
||||
"would_execute": True,
|
||||
"actor": identity,
|
||||
"role": active_role,
|
||||
@@ -24107,20 +24241,20 @@ def gitea_break_glass_restart(
|
||||
"confirmation": clean_confirmation,
|
||||
"disrupted_sessions_count": disrupted_count,
|
||||
"disrupted_sessions": disrupted_sessions,
|
||||
"audit_record": audit_payload,
|
||||
"saved_audit": dict(saved_audit or audit_payload),
|
||||
"audit_record": audit_event,
|
||||
"saved_audit": audit_event,
|
||||
"incident_issue": incident_issue_result,
|
||||
"reconciliation_required": True,
|
||||
"reconciliation_tool": "gitea_reconcile_after_restart",
|
||||
"follow_up_issue_required": True,
|
||||
"cross_references": ["#652", "#653", "#655", "#630", "#658", "#662", "#664"],
|
||||
"reasons": [
|
||||
"break-glass restart dry-run evaluated successfully" if dry_run
|
||||
else "break-glass restart executed with incident creation and mandatory reconciliation"
|
||||
"break-glass restart executed with incident creation and mandatory reconciliation"
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
# --- #662 post-restart reconciliation ---------------------------------------
|
||||
|
||||
_POST_RESTART_LAST_PROOF: dict | None = None
|
||||
|
||||
Reference in New Issue
Block a user