mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-05 00:34:09 +09:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
32
tests/integration/api_auth_test.go
Normal file
32
tests/integration/api_auth_test.go
Normal 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"`)
|
||||||
|
}
|
||||||
152
tests/integration/api_issue_dependency_test.go
Normal file
152
tests/integration/api_issue_dependency_test.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user