Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl]
indent_style = space

[templates/shared/actions/runner_badge_*.tmpl]
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
charset = unset

[Makefile]
indent_style = tab

Expand Down
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