feat: authorize and audit comment deletion (Closes #779)
CI / lint (pull_request) Successful in 1m28s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-13 22:55:47 +00:00
1 parent 9d14fa5ffb
commit 3896f17a58
5 files changed
+285 -6

No files matched your search

+64 -4
View File
@@ -412,6 +412,7 @@ StepUpAction = Literal[
"merge_pull",
"submit_pull_review",
"close_issue",
"delete_comment",
"log_recap_time",
"revoke_device",
"revoke_all_sessions",
@@ -4452,25 +4453,66 @@ async def _edit_conversation_comment(
async def _delete_conversation_comment(
repository: str, number: int, comment_id: int
request: Request,
step_up_grant: str | None,
repository: str,
number: int,
comment_id: int,
) -> JSONResponse:
target = f"{repository}#{number}:{comment_id}"
await _require_step_up(
request,
step_up_grant,
action="delete_comment",
target=target,
)
try:
journal = _security_event_store()
operation_id = await asyncio.to_thread(
journal.reserve,
"comment_deleted",
target=target,
)
except SecurityEventStoreError:
return JSONResponse(
{"error": "Security activity is temporarily unavailable. The comment was not deleted."},
status_code=503,
headers={"Retry-After": "1"},
)
try:
result = await asyncio.wait_for(
gitea_proxy.delete_owned_comment(repository, number, comment_id),
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
)
except gitea_proxy.CommentMutationForbiddenError as exc:
try:
await asyncio.to_thread(journal.discard, operation_id)
except SecurityEventStoreError:
pass
raise HTTPException(
status_code=403, detail="You can only change your own comments"
) from exc
except HTTPException:
try:
await asyncio.to_thread(journal.discard, operation_id)
except SecurityEventStoreError:
pass
raise
except Exception:
try:
await asyncio.to_thread(journal.discard, operation_id)
except SecurityEventStoreError:
pass
return JSONResponse(
{"error": "The comment deletion could not be confirmed. Please retry."},
status_code=503,
headers={"Retry-After": "1"},
)
try:
await asyncio.to_thread(journal.finalize, operation_id)
except SecurityEventStoreError:
# Upstream deletion is authoritative; pending evidence remains truthful.
pass
return JSONResponse(result)
@@ -4490,15 +4532,21 @@ async def edit_assigned_issue_comment(
@app.delete("/api/v1/repos/{owner}/{repo}/issues/{number}/comments/{comment_id}")
async def delete_assigned_issue_comment(
request: Request,
owner: str,
repo: str,
number: int = PathParam(gt=0),
comment_id: int = PathParam(gt=0),
step_up_grant: str | None = Header(
default=None, alias="X-Step-Up-Grant", max_length=128
),
):
repository = f"{owner}/{repo}"
if not await gitea_proxy.is_assigned_issue(repository, number):
raise HTTPException(status_code=404, detail="Assigned issue not found")
return await _delete_conversation_comment(repository, number, comment_id)
return await _delete_conversation_comment(
request, step_up_grant, repository, number, comment_id
)
@app.patch("/api/v1/repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}")
@@ -4517,15 +4565,21 @@ async def edit_assigned_pull_comment(
@app.delete("/api/v1/repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}")
async def delete_assigned_pull_comment(
request: Request,
owner: str,
repo: str,
number: int = PathParam(gt=0),
comment_id: int = PathParam(gt=0),
step_up_grant: str | None = Header(
default=None, alias="X-Step-Up-Grant", max_length=128
),
):
repository = f"{owner}/{repo}"
if not await gitea_proxy.is_assigned_pull(repository, number):
raise HTTPException(status_code=404, detail="Assigned pull request not found")
return await _delete_conversation_comment(repository, number, comment_id)
return await _delete_conversation_comment(
request, step_up_grant, repository, number, comment_id
)
@app.patch("/api/v1/notifications/{thread_id}/comments/{comment_id}")
@@ -4543,14 +4597,20 @@ async def edit_notification_comment(
@app.delete("/api/v1/notifications/{thread_id}/comments/{comment_id}")
async def delete_notification_comment(
request: Request,
thread_id: int = PathParam(gt=0),
comment_id: int = PathParam(gt=0),
step_up_grant: str | None = Header(
default=None, alias="X-Step-Up-Grant", max_length=128
),
):
try:
repository, number = await gitea_proxy.notification_conversation_target(thread_id)
except Exception as exc:
raise HTTPException(status_code=404, detail="Notification conversation not found") from exc
return await _delete_conversation_comment(repository, number, comment_id)
return await _delete_conversation_comment(
request, step_up_grant, repository, number, comment_id
)
@app.post("/api/v1/repos/{owner}/{repo}/issues/{number}/attachments", status_code=201)