refactor(api): load assignees list correctly (#39917)

Correctly handle empty names, upper&lower cases, duplicate names.

Fixes https://github.com/go-gitea/gitea/issues/39916

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Shivansh Gargandwxiaoguang authored and GitHub committed 2026-10-21 16:15:43 +00:00
1 parent 33610142c3
commit 0948aed411
7 files changed
+98 -104

No files matched your search

+36 -27
View File
@@ -14,9 +14,9 @@ import (
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
issue_indexer "gitea.dev/modules/indexer/issues"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
@@ -526,6 +526,39 @@ func GetIssue(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
func loadAssigneeUserIDs(ctx *context.APIContext, oneAssignee string, multipleAssignees []string) []int64 {
assigneeNames := container.Set[string]{}
assigneeNames.Add(strings.ToLower(oneAssignee))
for _, assignee := range multipleAssignees {
assigneeNames.Add(strings.ToLower(assignee))
}
assigneeNames.Remove("")
assignees, err := user_model.GetDoerUsersByNames(ctx, assigneeNames.Values())
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: %s", err))
} else {
ctx.APIErrorInternal(err)
}
return nil
}
// Check if the passed assignees is assignable
assigneeIDs := make([]int64, 0, len(assignees))
for _, assignee := range assignees {
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorAuto(err)
return nil
} else if !valid {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("User %s cannot be assigned to this issue", assignee.Name))
return nil
}
assigneeIDs = append(assigneeIDs, assignee.ID)
}
return assigneeIDs
}
// CreateIssue create an issue of a repository
func CreateIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
@@ -583,34 +616,10 @@ func CreateIssue(ctx *context.APIContext) {
return
}
issue.MilestoneID = form.Milestone
assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: %s", err))
} else {
ctx.APIErrorInternal(err)
}
assigneeIDs = loadAssigneeUserIDs(ctx, form.Assignee, form.Assignees)
if ctx.Written() {
return
}
// Check if the passed assignees is assignable
for _, aID := range assigneeIDs {
assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}.Error())
return
}
}
} else {
// setting labels is not allowed if user is not a writer
form.Labels = make([]int64, 0)
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"testing"
"gitea.dev/models/unittest"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestLoadAssigneeUserIDs(t *testing.T) {
unittest.PrepareTestEnv(t)
for _, test := range []struct {
name string
oneAssignee string
multipleAssignees []string
wantIDs []int64
wantStatus int
}{
{name: "empty", wantIDs: []int64{}, wantStatus: http.StatusOK},
{name: "combined", oneAssignee: "User2", multipleAssignees: []string{"user2", "USER2", "user1", ""}, wantIDs: []int64{1, 2}, wantStatus: http.StatusOK},
{name: "unknown user", oneAssignee: "does-not-exist", wantStatus: http.StatusUnprocessableEntity},
{name: "no access", oneAssignee: "user4", wantStatus: http.StatusUnprocessableEntity},
} {
t.Run(test.name, func(t *testing.T) {
ctx, resp := contexttest.MockAPIContext(t, "user2/repo2")
contexttest.LoadRepo(t, ctx, 2)
ids := loadAssigneeUserIDs(ctx, test.oneAssignee, test.multipleAssignees)
assert.Equal(t, test.wantStatus, resp.Code)
assert.ElementsMatch(t, test.wantIDs, ids)
})
}
}
+2 -25
View File
@@ -524,33 +524,10 @@ func CreatePullRequest(ctx *context.APIContext) {
pr.AllowMaintainerEdit = optional.FromPtr(form.AllowMaintainerEdit).ValueOrDefault(unitPullRequest.PullRequestsConfig().DefaultAllowMaintainerEdit)
// Get all assignee IDs
assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else {
ctx.APIErrorInternal(err)
}
assigneeIDs := loadAssigneeUserIDs(ctx, form.Assignee, form.Assignees)
if ctx.Written() {
return
}
// Check if the passed assignees is assignable
for _, aID := range assigneeIDs {
assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error())
return
}
}
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,