feat: authorize and audit comment deletion (Closes #779)
This commit is contained in:
1 parent
9d14fa5ffb
commit
3896f17a58
5 files changed
+285
-6
No files matched your search
+64
-4
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user