Fix #205: restrict "Submit Review" options to what the user may actually do - #342
Conversation
…lly do Users who are neither the change request author nor the article owner (nor anyone with write access) were offered all four review options. "Approve" silently failed and "Close" returned a 500 because there is no "close" review type server-side. Random users now only get the "Comment" option: "Approve", "Request changes" and "Close" are no longer rendered for them, in the same spirit as #192, and the server rejects such submissions with a 403 carrying an error message instead of failing silently or with a 500. Behaviour for the article owner / users with write access and for the change request author (greyed-out approve/reject, per #192) is unchanged. Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
/review AI review started. |
Advanced AI Review
Click to expand reviewLet me try to empirically confirm the "close → 500 for author/owner" path by running the new tests.Static evidence is conclusive for the main finding; no need for a multi-minute build. Let me do final checks on the API route middleware.# Review: Fix #205 — restrict "Submit Review" options 1. 🔴 The "Close" option still 500s for everyone it is now shown to
The permission gate lets write-access users and the change-request author through for
case "close":
if !isPoster {
jsonForbidden(ctx, ctx.Locale.TrString("repo.issues.review.no_permission.close"))
return false
}
reviewType := form.ReviewType()
switch reviewType {
case issues_model.ReviewTypeUnknown:
ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
return
func (f SubmitReviewForm) ReviewType() issues_model.ReviewType {
switch f.Type {
case "approve":
return issues_model.ReviewTypeApprove
case "comment":
return issues_model.ReviewTypeComment
case "reject":
return issues_model.ReviewTypeReject
case "":
return issues_model.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form
default:
return issues_model.ReviewTypeUnknown
}
}( Yet the modal still advertises the option to exactly those users, with {{if $canReviewDecision}}
<label class="review-radio-option">
<input type="radio" name="type" value="close">diff.review.close_desc = Submit feedback and close the change request without approving.So the article owner — the primary user of this feature — selects "Close", and gets a 500 error page. The PR description's claim "This turns the old 500 on Close into a clean rejection" only holds for random users. Suggested fix (Option A — implement it, matching the advertised semantics "submit feedback and close"), in reviewType := form.ReviewType()
if form.Type == "close" {
// "Close" submits the pending review as a comment and then closes the
// change request, see `diff.review.close_desc`.
reviewType = issues_model.ReviewTypeComment
}and after the successful if form.Type == "close" && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
ctx.ServerError("CloseIssue", err)
return
}
}( Option B — if "Close" from the review modal is not actually wanted, delete the radio option, the 2. 🟡 New test masks the remaining 500 with
|
- derive the permission decision from `form.ReviewType()` instead of the raw form strings, so renaming a form value can no longer silently fail open; only "close", which has no `ReviewType` of its own, still matches the raw value - replace the package-local `jsonForbidden` helper with a `(*context.Context).JSONForbidden` sibling of `JSONError`/`JSONErrorNotFound` - answer an unknown review type with a 400 `JSONError` instead of a 500 - template: rename `$showSelfTooltip` to `$isChangeRequestAuthor` now that it is also a permission input, and merge the duplicated approve/reject guard - tests: use the exported `HTMLDoc.Find` helper and assert the `disabled` attribute the author's approve/reject options are supposed to carry Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the CI review on PR #350: - models/issues/review.go no longer reports ContentEmptyErr for a review that carries attachments (both the new-review and the pending-review branch), the same rule Fix #321 established for plain comments. Without it, submitting a close review with an upload but no text silently discarded the upload, since submitCloseReview deliberately swallows ContentEmptyErr. - A close attempt on a merged change request now uses a dedicated repo.pulls.close_blocked_merged string instead of repo.pulls.has_merged, whose wording is about merging, not closing. - A forbidden close returns a JSON errorMessage instead of a bare HTML error page, so the fetch-action modal can render the reason as a toast. - The "Close" radio is hidden on an already-closed change request, where it degraded into a plain comment review. - submitCloseReview drops the dead comm = nil, the redundant LoadPullRequest (GetActionIssue -> LoadAttributes already loads it, as NewComment relies on) and the redundant ctx.Doer != nil; the permission check now runs first so an unauthorized caller triggers no work. The attachments block is hoisted into SubmitReview and passed in, instead of being duplicated per branch. - The test uses the existing GetUserCSRFToken helper instead of a local closure and asserts the forbidden response carries errorMessage. canCloseChangeRequest and its NOTE are kept as-is: PR #342 is still open, its insert points are disjoint, and the rebase there stays a single deletion of this one helper. Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The JSONError added while addressing review feedback used fmt.Sprintf with a single trailing %s, which perfsprint rejects in favour of concatenation. This was failing lint-backend, lint-go-gogit and lint-go-windows on PR #342; the tests themselves were green. Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Approved |
#342 landed on master, so the redundant permission check this branch carried while that PR was open is now the collision it was expected to be. Resolved as planned: - dropped canCloseChangeRequest; SubmitReview now runs master's checkReviewDecisionPermission first, which already restricts "close" to users with write access plus the change request author - submitCloseReview keeps its own doc note about that precondition instead of re-checking it - the "Close" radio is gated on `and $canReviewDecision (not $.Issue.IsClosed)`, combining #342's permission gate with #347's already-closed guard, matching how master gates approve/reject - kept both sets of integration tests; they pass together - dropped the duplicate issues.review.no_permission.close locale key that both branches had added Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…500 (#350) * Fix #347: implement the "Close" review option instead of a 500 The review modal's "Close" option had no server-side counterpart: the form type mapped to ReviewTypeUnknown and SubmitReview bailed out with ctx.ServerError, so anyone allowed to pick the option got an HTTP 500. "close" now maps to a plain comment review and SubmitReview delegates to a dedicated handler that posts the feedback and then closes the change request through issue_service.CloseIssue, i.e. the same path as the "Close" button on the change request, so timeline comments, notifications and state stay consistent. An empty review body is allowed: the review is skipped and the change request is closed anyway. Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix #347: address the AI review on the "Close" review option Follow-up to the CI review on PR #350: - models/issues/review.go no longer reports ContentEmptyErr for a review that carries attachments (both the new-review and the pending-review branch), the same rule Fix #321 established for plain comments. Without it, submitting a close review with an upload but no text silently discarded the upload, since submitCloseReview deliberately swallows ContentEmptyErr. - A close attempt on a merged change request now uses a dedicated repo.pulls.close_blocked_merged string instead of repo.pulls.has_merged, whose wording is about merging, not closing. - A forbidden close returns a JSON errorMessage instead of a bare HTML error page, so the fetch-action modal can render the reason as a toast. - The "Close" radio is hidden on an already-closed change request, where it degraded into a plain comment review. - submitCloseReview drops the dead comm = nil, the redundant LoadPullRequest (GetActionIssue -> LoadAttributes already loads it, as NewComment relies on) and the redundant ctx.Doer != nil; the permission check now runs first so an unauthorized caller triggers no work. The attachments block is hoisted into SubmitReview and passed in, instead of being duplicated per branch. - The test uses the existing GetUserCSRFToken helper instead of a local closure and asserts the forbidden response carries errorMessage. canCloseChangeRequest and its NOTE are kept as-is: PR #342 is still open, its insert points are disjoint, and the rebase there stays a single deletion of this one helper. Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #205
Problem
A "random user" — neither the change request author nor the article owner / someone with write access — was offered all four options in the Submit Review modal (
Comment,Approve,Request changes,Close).Approvesilently did nothing andClosereturned a 500 (there is noclosereview type server-side, so it fell through toReviewTypeUnknown→ctx.ServerError).Decision
Random users get
Commentonly.Approve,Request changesandCloseare all unavailable to them.Changes
templates/repo/diff/review_submit.tmpl: theApprove/Request changes/Closeoptions are now only rendered when the viewer has write access to the change request or is its author ($canReviewDecision). They are hidden, not merely disabled — same spirit as Fix 180: Improve UX of "Submit review" modal #192.routers/web/repo/pull_review.go: server-side enforcement inSubmitReview.approve/rejectrequire write access;closeadditionally allows the change request author. Anything else gets a 403 with anerrorMessagepayload, which theform-fetch-actionfrontend renders as an error toast. This turns the old 500 onCloseinto a clean rejection.custom/options/locale/locale_en-US.ini: three newrepo.issues.review.no_permission.*strings.tests/integration/pull_review_test.go: two new integration tests — one asserting 403 forapprove/reject/closeand 200 forcommentfrom a random user (and that the author is not blocked by the new check), one asserting the rendered radio options per role.Unchanged
Article owner / users with write access keep all four options. The change request author keeps the greyed-out
Approve/Request changesoptions andClose, exactly as #192 left them.Testing
make test-sqlite#TestPullView_SubmitReview— both new tests pass.Model used: Claude Opus 5 (1M context)
🤖 Generated with Claude Code