fix(api): lock and unlock issues as the signed-in user, not the repository owner (#40291)

`PUT/DELETE /repos/{owner}/{repo}/issues/{index}/lock` passed
`ctx.ContextUser` as the doer of `LockIssue` / `UnlockIssue`. In the
repo routes that is the owner of the repository, so on a repository of
an organization the lock and unlock timeline comments were created by
the organization instead of the user who did it. The web handler already
uses the signed-in user; this uses `ctx.Doer` as well.

Adds a subtest `DoerOnOrganizationRepo` to the existing
`TestAPILockIssue`: a member of an organization locks and unlocks an
issue of an organization repository, and the comments must be posted by
that member.

Found while testing audit logging of the issue lock events downstream.

_Authored by Codet (Claude)._
This commit is contained in:
Lunny Xiao
2026-11-09 18:00:06 +00:00
committed by GitHub
parent 772f365254
commit bccc90d654
2 changed files with 25 additions and 2 deletions

View File

@@ -61,7 +61,7 @@ func LockIssue(ctx *context.APIContext) {
if !issue.IsLocked {
opt := &issues_model.IssueLockOptions{
Doer: ctx.ContextUser,
Doer: ctx.Doer,
Issue: issue,
Reason: reason,
}
@@ -121,7 +121,7 @@ func UnlockIssue(ctx *context.APIContext) {
if issue.IsLocked {
opt := &issues_model.IssueLockOptions{
Doer: ctx.ContextUser,
Doer: ctx.Doer,
Issue: issue,
}

View File

@@ -17,6 +17,7 @@ import (
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPILockIssue(t *testing.T) {
@@ -71,4 +72,26 @@ func TestAPILockIssue(t *testing.T) {
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token34)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DoerOnOrganizationRepo", func(t *testing.T) {
// the lock and unlock comments are posted by the signed-in user, not by the organization owning the repository
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
require.True(t, org.IsOrganization())
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) // an owner of the organization
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/lock", org.Name, repo.Name, issue.Index)
req := NewRequestWithJSON(t, "PUT", urlStr, api.LockIssueOption{Reason: "Spam"}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
lockComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issue.ID, Type: issues_model.CommentTypeLock})
assert.Equal(t, doer.ID, lockComment.PosterID)
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unlockComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issue.ID, Type: issues_model.CommentTypeUnlock})
assert.Equal(t, doer.ID, unlockComment.PosterID)
})
}