Fix various permission & login related bugs (#36002)

Permission & protection check:

- Fix Delete Release permission check
- Fix Update Pull Request with rebase branch protection check
- Fix Issue Dependency permission check
- Fix Delete Comment History ID check

Information leaking:

- Show unified message for non-existing user and invalid password
    - Fix #35984
- Don't expose release draft to non-writer users.
- Make API returns signature's email address instead of the user
profile's.

Auth & Login:

- Avoid GCM OAuth2 attempt when OAuth2 is disabled
    - Fix #35510

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiao
2025-11-21 23:16:08 -08:00
committed by GitHub
parent a60a8c6966
commit 62d750eadb
18 changed files with 385 additions and 61 deletions

View File

@@ -81,6 +81,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/admin"
@@ -774,7 +775,9 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
ar, err := common.AuthShared(ctx.Base, nil, authMethod) ar, err := common.AuthShared(ctx.Base, nil, authMethod)
if err != nil { if err != nil {
ctx.APIError(http.StatusUnauthorized, err) msg, ok := auth.ErrAsUserAuthMessage(err)
msg = util.Iif(ok, msg, "invalid username, password or token")
ctx.APIError(http.StatusUnauthorized, msg)
return return
} }
ctx.Doer = ar.Doer ctx.Doer = ar.Doer

View File

@@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
return return
} }
dependencyPerm := getPermissionForRepo(ctx, target.Repo) dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() { if ctx.Written() {
return return
} }
@@ -262,7 +262,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
return return
} }
dependencyPerm := getPermissionForRepo(ctx, target.Repo) dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() { if ctx.Written() {
return return
} }

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release" release_service "code.gitea.io/gitea/services/release"
@@ -58,6 +59,13 @@ func GetReleaseByTag(ctx *context.APIContext) {
return return
} }
if release.IsDraft { // only the users with write access can see draft releases
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
ctx.APIErrorNotFound()
return
}
}
if err = release.LoadAttributes(ctx); err != nil { if err = release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return

View File

@@ -146,7 +146,13 @@ func httpBase(ctx *context.Context) *serviceHandler {
// rely on the results of Contexter // rely on the results of Contexter
if !ctx.IsSigned { if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit // TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`) if setting.OAuth2.Enabled {
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
} else {
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
}
ctx.HTTPError(http.StatusUnauthorized) ctx.HTTPError(http.StatusUnauthorized)
return nil return nil
} }

View File

@@ -206,12 +206,11 @@ func SoftDeleteContentHistory(ctx *context.Context) {
ctx.NotFound(issues_model.ErrCommentNotExist{}) ctx.NotFound(issues_model.ErrCommentNotExist{})
return return
} }
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if commentID != 0 { if commentID != 0 {
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil { if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err) log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return return

View File

@@ -5,6 +5,7 @@
package auth package auth
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@@ -40,6 +41,20 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
} }
}) })
type ErrUserAuthMessage string
func (e ErrUserAuthMessage) Error() string {
return string(e)
}
func ErrAsUserAuthMessage(err error) (string, bool) {
var msg ErrUserAuthMessage
if errors.As(err, &msg) {
return msg.Error(), true
}
return "", false
}
// Init should be called exactly once when the application starts to allow plugins // Init should be called exactly once when the application starts to allow plugins
// to allocate necessary resources // to allocate necessary resources
func Init() { func Init() {

View File

@@ -5,7 +5,6 @@
package auth package auth
import ( import (
"errors"
"net/http" "net/http"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
@@ -146,7 +145,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err return nil, err
} }
if hasWebAuthn { if hasWebAuthn {
return nil, errors.New("basic authorization is not allowed while WebAuthn enrolled") return nil, ErrUserAuthMessage("basic authorization is not allowed while WebAuthn enrolled")
} }
if err := validateTOTP(req, u); err != nil { if err := validateTOTP(req, u); err != nil {

View File

@@ -542,8 +542,9 @@ func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerifi
} }
if verif.SigningUser != nil { if verif.SigningUser != nil {
commitVerification.Signer = &api.PayloadUser{ commitVerification.Signer = &api.PayloadUser{
Name: verif.SigningUser.Name, UserName: verif.SigningUser.Name,
Email: verif.SigningUser.Email, Name: verif.SigningUser.DisplayName(),
Email: verif.SigningEmail, // Use the email from the signature, not from the user profile
} }
} }
return commitVerification return commitVerification

View File

@@ -547,11 +547,15 @@ var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections // IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) { func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
return isUserAllowedToMergeInRepoBranch(ctx, pr.BaseRepoID, pr.BaseBranch, p, user)
}
func isUserAllowedToMergeInRepoBranch(ctx context.Context, repoID int64, branch string, p access_model.Permission, user *user_model.User) (bool, error) {
if user == nil { if user == nil {
return false, nil return false, nil
} }
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repoID, branch)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -101,11 +101,11 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
} }
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
// update PR means send new commits to PR head branch from base branch
func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) { func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
if pull.Flow == issues_model.PullRequestFlowAGit { if pull.Flow == issues_model.PullRequestFlowAGit {
return false, false, nil return false, false, nil
} }
if user == nil { if user == nil {
return false, false, nil return false, false, nil
} }
@@ -121,54 +121,46 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
return false, false, err return false, false, err
} }
pr := &issues_model.PullRequest{ // 1. check base repository's AllowRebaseUpdate configuration
HeadRepoID: pull.BaseRepoID, // it is a config in base repo but controls the head (fork) repo's "Update" behavior
HeadRepo: pull.BaseRepo, {
BaseRepoID: pull.HeadRepoID, prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
BaseRepo: pull.HeadRepo,
HeadBranch: pull.BaseBranch,
BaseBranch: pull.HeadBranch,
}
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, false, err
}
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, false, err
}
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
if repo_model.IsErrUnitTypeNotExist(err) { if repo_model.IsErrUnitTypeNotExist(err) {
return false, false, nil return false, false, nil // the PR unit is disabled in base repo
} else if err != nil {
return false, false, fmt.Errorf("get base repo unit: %v", err)
} }
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) rebaseAllowed = prBaseUnit.PullRequestsConfig().AllowRebaseUpdate
return false, false, err
} }
rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate // 2. check head branch protection whether rebase is allowed, if pb not found then rebase depends on the above setting
{
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push) pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.HeadRepoID, pull.HeadBranch)
if pb != nil { if err != nil {
pb.Repo = pull.BaseRepo return false, false, err
if !pb.CanUserForcePush(ctx, user) { }
rebaseAllowed = false // If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
if pb != nil {
pb.Repo = pull.HeadRepo
rebaseAllowed = rebaseAllowed && pb.CanUserForcePush(ctx, user)
} }
} }
// 3. check whether user has write access to head branch
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user) baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
mergeAllowed, err = IsUserAllowedToMerge(ctx, pr, headRepoPerm, user) mergeAllowed, err = isUserAllowedToMergeInRepoBranch(ctx, pull.HeadRepoID, pull.HeadBranch, headRepoPerm, user)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
// 4. if the pull creator allows maintainer to edit, it means the write permissions of the head branch has been
// granted to the user with write permission of the base repository
if pull.AllowMaintainerEdit { if pull.AllowMaintainerEdit {
mergeAllowedMaintainer, err := IsUserAllowedToMerge(ctx, pr, baseRepoPerm, user) mergeAllowedMaintainer, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
@@ -176,6 +168,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
mergeAllowed = mergeAllowed || mergeAllowedMaintainer mergeAllowed = mergeAllowed || mergeAllowedMaintainer
} }
// if merge is not allowed, rebase is also not allowed
rebaseAllowed = rebaseAllowed && mergeAllowed
return mergeAllowed, rebaseAllowed, nil return mergeAllowed, rebaseAllowed, nil
} }

View File

@@ -361,7 +361,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil { if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err) return fmt.Errorf("GetProtectedTags: %w", err)
} }
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID) isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2")
MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2", "wrong-password")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user-not-exist")
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
req = NewRequestf(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth("Bearer wrong_token")
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
}

View File

@@ -0,0 +1,152 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func enableRepoDependencies(t *testing.T, repoID int64) {
t.Helper()
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
repoUnit.IssuesConfig().EnableDependencies = true
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
}
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}
func TestAPIDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}

View File

@@ -15,6 +15,8 @@ import (
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -269,6 +271,42 @@ func TestAPIGetReleaseByTag(t *testing.T) {
assert.NotEmpty(t, err.Message) assert.NotEmpty(t, err.Message)
} }
func TestAPIGetDraftReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
tag := "draft-release"
// anonymous should not be able to get draft release
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag))
MakeRequest(t, req, http.StatusNotFound)
// user 40 should be able to get draft release because he has write access to the repository
token := getUserToken(t, "user40", auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
release := api.Release{}
DecodeJSON(t, resp, &release)
assert.Equal(t, "draft-release", release.Title)
// remove user 40 access from the repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
// user 40 should not be able to get draft release
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// user 2 should be able to get draft release because he is the publisher
user2Token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(user2Token)
resp = MakeRequest(t, req, http.StatusOK)
release = api.Release{}
DecodeJSON(t, resp, &release)
assert.Equal(t, "draft-release", release.Title)
}
func TestAPIDeleteReleaseByTagName(t *testing.T) { func TestAPIDeleteReleaseByTagName(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()

View File

@@ -41,17 +41,6 @@ func TestAPIUserReposNotLogin(t *testing.T) {
} }
} }
func TestAPIUserReposWithWrongToken(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
wrongToken := "Bearer " + "wrong_token"
req := NewRequestf(t, "GET", "/api/v1/users/%s/repos", user.Name).
AddTokenAuth(wrongToken)
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), "user does not exist")
}
func TestAPISearchRepo(t *testing.T) { func TestAPISearchRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
const keyword = "test" const keyword = "test"

View File

@@ -271,8 +271,8 @@ type RequestWrapper struct {
*http.Request *http.Request
} }
func (req *RequestWrapper) AddBasicAuth(username string) *RequestWrapper { func (req *RequestWrapper) AddBasicAuth(username string, password ...string) *RequestWrapper {
req.Request.SetBasicAuth(username, userPassword) req.Request.SetBasicAuth(username, util.OptionalArg(password, userPassword))
return req return req
} }

View File

@@ -11,7 +11,11 @@ import (
"time" "time"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
@@ -58,6 +62,14 @@ func TestAPIPullUpdate(t *testing.T) {
}) })
} }
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
t.Helper()
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
repoUnit.PullRequestsConfig().AllowRebaseUpdate = allow
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
}
func TestAPIPullUpdateByRebase(t *testing.T) { func TestAPIPullUpdateByRebase(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// Create PR to test // Create PR to test
@@ -73,10 +85,32 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
assert.Equal(t, 1, diffCount.Ahead) assert.Equal(t, 1, diffCount.Ahead)
assert.NoError(t, pr.LoadIssue(t.Context())) assert.NoError(t, pr.LoadIssue(t.Context()))
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
session := loginUser(t, "user2") session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index). req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token) AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
// use a user which have write access to the pr but not write permission to the head repository to do the rebase
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
token40 := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteRepository)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusForbidden)
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusOK) session.MakeRequest(t, req, http.StatusOK)
// Test GetDiverging after update // Test GetDiverging after update
@@ -87,6 +121,49 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
}) })
} }
func TestAPIPullUpdateByRebase2(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// Create PR to test
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
pr := createOutdatedPR(t, user, org26)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
assert.NoError(t, pr.LoadIssue(t.Context()))
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
// add a protected branch rule to the head branch to block rebase
pb := git_model.ProtectedBranch{
RepoID: pr.HeadRepo.ID,
RuleName: pr.HeadBranch,
CanPush: false,
CanForcePush: false,
}
err := git_model.UpdateProtectBranch(t.Context(), pr.HeadRepo, &pb, git_model.WhitelistOptions{})
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
// remove the protected branch rule to allow rebase
err = git_model.DeleteProtectedBranch(t.Context(), pr.HeadRepo, pb.ID)
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusOK)
})
}
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest { func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{ baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
Name: "repo-pr-update", Name: "repo-pr-update",

View File

@@ -149,12 +149,18 @@ func TestRepushTag(t *testing.T) {
// delete the tag // delete the tag
_, _, err = gitcmd.NewCommand("push", "origin", "--delete", "v2.0").WithDir(dstPath).RunStdString(t.Context()) _, _, err = gitcmd.NewCommand("push", "origin", "--delete", "v2.0").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err) assert.NoError(t, err)
// query the release by API and it should be a draft
// query the release by API with no auth and it should be 404
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")) req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
MakeRequest(t, req, http.StatusNotFound)
// query the release by API and it should be a draft
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
var respRelease *api.Release var respRelease *api.Release
DecodeJSON(t, resp, &respRelease) DecodeJSON(t, resp, &respRelease)
assert.True(t, respRelease.IsDraft) assert.True(t, respRelease.IsDraft)
// re-push the tag // re-push the tag
_, _, err = gitcmd.NewCommand("push", "origin", "--tags", "v2.0").WithDir(dstPath).RunStdString(t.Context()) _, _, err = gitcmd.NewCommand("push", "origin", "--tags", "v2.0").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err) assert.NoError(t, err)