fix(api): check unit read permission in issue sub-resource endpoints (#39885) (#39938)

Backport #39885 by @basavaraj-sm05

Several `/repos/{owner}/{repo}/issues/{index}/...` API endpoints load
the issue by index without checking
`CanReadIssuesOrPulls(issue.IsPull)`, unlike `GetIssue`,
`ListIssueComments` and `GetIssueReactions`. The route group's
`mustEnableIssuesOrPulls` passes when the caller can read either unit,
so someone who can read issues but not pull requests (a team with only
the Issues unit, or per-unit anonymous access on a public repo) can read
a pull request's whole conversation through `/timeline`, plus its
labels, subscribers, attachments and tracked times, and can subscribe to
it. The reverse holds for issues.

These handlers now return 404 in that case, like their siblings.

Co-authored-by: Basavaraj S m <basavaraj@digiscrypt.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot
2026-10-22 05:27:46 -07:00
committed by GitHub
co-authored by Basavaraj S m silverwind wxiaoguang
parent 3996c2c520
commit 3e5256b6ca
12 changed files with 135 additions and 214 deletions
+26 -29
View File
@@ -513,17 +513,12 @@ func GetIssue(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
@@ -698,19 +693,13 @@ func EditIssue(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.EditIssueOption](ctx)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
issue.Repo = ctx.Repo.Repository
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
err = issue.LoadAttributes(ctx)
err := issue.LoadAttributes(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -901,13 +890,12 @@ func DeleteIssue(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
if err = issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
if err := issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
}
@@ -953,13 +941,8 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm[*api.EditDeadlineOption](ctx)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -999,3 +982,17 @@ func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, stat
}
}
}
func getRepoIssueByIndexForDoer(ctx *context.APIContext, index int64) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
+4 -11
View File
@@ -146,14 +146,8 @@ func CheckIssueAssignee(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -187,9 +181,8 @@ func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *rep
}
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
+4 -16
View File
@@ -57,7 +57,7 @@ func GetIssueAttachment(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/error"
issue := getIssueFromContext(ctx)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -100,7 +100,7 @@ func ListIssueAttachments(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/error"
issue := getIssueFromContext(ctx)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -163,7 +163,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// "423":
// "$ref": "#/responses/repoArchivedError"
issue := getIssueFromContext(ctx)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -333,20 +333,8 @@ func DeleteIssueAttachment(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
issue := getIssueFromContext(ctx)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return nil
}
+6 -21
View File
@@ -69,17 +69,10 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{
IssueID: issue.ID,
@@ -173,12 +166,10 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
@@ -381,14 +372,8 @@ func CreateIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm[*api.CreateIssueCommentOption](ctx)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"testing"
perm_model "gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"gitea.dev/services/context"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestIssueReadEndpointsRequireUnitPermission(t *testing.T) {
unittest.PrepareTestEnv(t)
for name, handler := range map[string]func(*context.APIContext){
"timeline": ListIssueCommentsAndTimeline,
"labels": ListIssueLabels,
"subscribers": GetIssueSubscribers,
"attachments": ListIssueAttachments,
} {
t.Run(name, func(t *testing.T) {
request := func(index string) int {
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx, 1)
ctx.Repo.Permission = access_model.Permission{}
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode([]*repo_model.RepoUnit{{Type: unit.TypeIssues}}, perm_model.AccessModeRead)
ctx.SetPathParam("index", index)
handler(ctx)
return resp.Code
}
assert.Equal(t, http.StatusOK, request("1"))
assert.Equal(t, http.StatusNotFound, request("2"))
})
}
}
+8 -30
View File
@@ -61,15 +61,8 @@ func GetIssueDependencies(ctx *context.APIContext) {
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
// 1. We must be able to read this issue
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -79,7 +72,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
// Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
@@ -177,7 +170,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
target := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if ctx.Written() {
return
}
@@ -238,7 +231,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
target := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if ctx.Written() {
return
}
@@ -303,16 +296,11 @@ func GetIssueBlocks(ctx *context.APIContext) {
// We need to list the issues that DEPEND on this issue not the other way round
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
issue := getParamsIssue(ctx)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if ctx.Written() {
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 1 {
@@ -399,7 +387,7 @@ func CreateIssueBlocking(ctx *context.APIContext) {
// "404":
// description: the issue does not exist
dependency := getParamsIssue(ctx)
dependency := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if ctx.Written() {
return
}
@@ -456,7 +444,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
dependency := getParamsIssue(ctx)
dependency := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if ctx.Written() {
return
}
@@ -480,16 +468,6 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
var repo *repo_model.Repository
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
+9 -29
View File
@@ -47,13 +47,8 @@ func ListIssueLabels(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -167,13 +162,8 @@ func DeleteIssueLabel(ctx *context.APIContext) {
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -292,13 +282,8 @@ func ClearIssueLabels(ctx *context.APIContext) {
// "423":
// "$ref": "#/responses/repoArchivedError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -316,14 +301,9 @@ func ClearIssueLabels(ctx *context.APIContext) {
}
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil, nil, err
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return nil, nil, errors.New("issue not found")
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
+6 -8
View File
@@ -51,9 +51,8 @@ func LockIssue(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
reason := web.GetForm[*api.LockIssueOption](ctx).Reason
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -70,7 +69,7 @@ func LockIssue(ctx *context.APIContext) {
}
issue.Repo = ctx.Repo.Repository
err = issues_model.LockIssue(ctx, opt)
err := issues_model.LockIssue(ctx, opt)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -114,9 +113,8 @@ func UnlockIssue(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -132,7 +130,7 @@ func UnlockIssue(ctx *context.APIContext) {
}
issue.Repo = ctx.Repo.Repository
err = issues_model.UnlockIssue(ctx, opt)
err := issues_model.UnlockIssue(ctx, opt)
if err != nil {
ctx.APIErrorInternal(err)
return
+9 -26
View File
@@ -41,20 +41,13 @@ func PinIssue(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else if issues_model.IsErrIssueMaxPinReached(err) {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
// If we don't do this, it will crash when trying to add the pin event to the comment history
err = issue.LoadRepo(ctx)
err := issue.LoadRepo(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -98,18 +91,13 @@ func UnpinIssue(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
// If we don't do this, it will crash when trying to add the unpin event to the comment history
err = issue.LoadRepo(ctx)
err := issue.LoadRepo(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -159,17 +147,12 @@ func MoveIssuePin(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
err = issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
err := issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
if err != nil {
ctx.APIErrorInternal(err)
return
+2 -7
View File
@@ -172,13 +172,8 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
}
func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return nil
}
+6 -24
View File
@@ -104,14 +104,8 @@ func DelIssueSubscription(ctx *context.APIContext) {
}
func setIssueSubscription(ctx *context.APIContext, watch bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -185,14 +179,8 @@ func CheckIssueSubscription(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -251,14 +239,8 @@ func GetIssueSubscribers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
+11 -13
View File
@@ -75,9 +75,8 @@ func ListTrackedTimes(ctx *context.APIContext) {
ctx.APIErrorNotFound("timetracker is disabled")
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -100,6 +99,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
opts.UserID = user.ID
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
@@ -200,9 +200,8 @@ func AddTime(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm[*api.AddTimeOption](ctx)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -219,6 +218,7 @@ func AddTime(ctx *context.APIContext) {
if form.User != "" {
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
// allow only RepoAdmin, Admin and User to add time
var err error
user, err = user_model.GetUserByName(ctx, form.User)
if err != nil {
ctx.APIErrorAuto(err)
@@ -280,9 +280,8 @@ func ResetIssueTime(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}
@@ -295,7 +294,7 @@ func ResetIssueTime(ctx *context.APIContext) {
return
}
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
err := issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err.Error())
@@ -349,9 +348,8 @@ func DeleteTime(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
if issue == nil {
return
}