Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
Expand Down Expand Up @@ -791,7 +792,9 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
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
}
ctx.Doer = ar.Doer
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/repo/issue_dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
return
}

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

dependencyPerm := getPermissionForRepo(ctx, target.Repo)
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() {
return
}
Expand Down
8 changes: 8 additions & 0 deletions routers/api/v1/repo/release_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"

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/convert"
release_service "code.gitea.io/gitea/services/release"
Expand Down Expand Up @@ -58,6 +59,13 @@ func GetReleaseByTag(ctx *context.APIContext) {
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 {
ctx.APIErrorInternal(err)
return
Expand Down
8 changes: 7 additions & 1 deletion routers/web/repo/githttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ func httpBase(ctx *context.Context) *serviceHandler {
// rely on the results of Contexter
if !ctx.IsSigned {
// 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)
return nil
}
Expand Down
9 changes: 4 additions & 5 deletions routers/web/repo/issue_content_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,11 @@ func SoftDeleteContentHistory(ctx *context.Context) {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if commentID != 0 {
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}

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)
return
Expand Down
15 changes: 15 additions & 0 deletions services/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package auth

import (
"errors"
"fmt"
"net/http"
"regexp"
Expand Down Expand Up @@ -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
// to allocate necessary resources
func Init() {
Expand Down
3 changes: 1 addition & 2 deletions services/auth/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package auth

import (
"errors"
"net/http"

actions_model "code.gitea.io/gitea/models/actions"
Expand Down Expand Up @@ -146,7 +145,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err
}
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 {
Expand Down
5 changes: 3 additions & 2 deletions services/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,9 @@ func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerifi
}
if verif.SigningUser != nil {
commitVerification.Signer = &api.PayloadUser{
Name: verif.SigningUser.Name,
Email: verif.SigningUser.Email,
UserName: verif.SigningUser.Name,
Name: verif.SigningUser.DisplayName(),
Email: verif.SigningEmail, // Use the email from the signature, not from the user profile
}
}
return commitVerification
Expand Down
6 changes: 5 additions & 1 deletion services/pull/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,11 +546,15 @@ var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)

// 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) {
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 {
return false, nil
}

pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repoID, branch)
if err != nil {
return false, err
}
Expand Down
59 changes: 27 additions & 32 deletions services/pull/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,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
// 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) {
if pull.Flow == issues_model.PullRequestFlowAGit {
return false, false, nil
}

if user == nil {
return false, false, nil
}
Expand All @@ -117,61 +117,56 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
return false, false, err
}

pr := &issues_model.PullRequest{
HeadRepoID: pull.BaseRepoID,
HeadRepo: pull.BaseRepo,
BaseRepoID: pull.HeadRepoID,
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 {
// 1. check base repository's AllowRebaseUpdate configuration
// it is a config in base repo but controls the head (fork) repo's "Update" behavior
{
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
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)
return false, false, err
rebaseAllowed = prBaseUnit.PullRequestsConfig().AllowRebaseUpdate
}

rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate

// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
if pb != nil {
pb.Repo = pull.BaseRepo
if !pb.CanUserForcePush(ctx, user) {
rebaseAllowed = false
// 2. check head branch protection whether rebase is allowed, if pb not found then rebase depends on the above setting
{
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.HeadRepoID, pull.HeadBranch)
if err != nil {
return false, false, err
}
// 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)
if err != nil {
return false, false, err
}

mergeAllowed, err = IsUserAllowedToMerge(ctx, pr, headRepoPerm, user)
mergeAllowed, err = isUserAllowedToMergeInRepoBranch(ctx, pull.HeadRepoID, pull.HeadBranch, headRepoPerm, user)
if err != nil {
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 {
mergeAllowedMaintainer, err := IsUserAllowedToMerge(ctx, pr, baseRepoPerm, user)
mergeAllowedMaintainer, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
if err != nil {
return false, false, err
}

mergeAllowed = mergeAllowed || mergeAllowedMaintainer
}

// if merge is not allowed, rebase is also not allowed
rebaseAllowed = rebaseAllowed && mergeAllowed

return mergeAllowed, rebaseAllowed, nil
}

Expand Down
2 changes: 1 addition & 1 deletion services/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil {
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 {
return err
}
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/api_auth_test.go
Original file line number Diff line number Diff line change
@@ -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"`)
}
Loading
Loading