diff --git a/.editorconfig b/.editorconfig index 13aa8d50f015b..bf1cf757cc6dc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5f52ee8a4323d..e5ffdc70ef5ea 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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" @@ -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 diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 1b58beb7b6e92..b34e325e5da23 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) { return } - dependencyPerm := getPermissionForRepo(ctx, target.Repo) + dependencyPerm := getPermissionForRepo(ctx, dependency.Repo) if ctx.Written() { return } @@ -262,7 +262,7 @@ func RemoveIssueDependency(ctx *context.APIContext) { return } - dependencyPerm := getPermissionForRepo(ctx, target.Repo) + dependencyPerm := getPermissionForRepo(ctx, dependency.Repo) if ctx.Written() { return } diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index b5e7d83b2adcf..8991e201d8bba 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -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" @@ -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 diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index ab649b6f15d86..a5b3ec2efebcc 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -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 } diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 3602f4ec8abbf..a56df78163d38 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -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 diff --git a/services/auth/auth.go b/services/auth/auth.go index fb6612290bc7a..291e78a7358a9 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -5,6 +5,7 @@ package auth import ( + "errors" "fmt" "net/http" "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 // to allocate necessary resources func Init() { diff --git a/services/auth/basic.go b/services/auth/basic.go index 6d147deeb1388..501924b4df32a 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -5,7 +5,6 @@ package auth import ( - "errors" "net/http" 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 } 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 { diff --git a/services/convert/convert.go b/services/convert/convert.go index 633edfed4b32e..400e49ad6d227 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -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 diff --git a/services/pull/merge.go b/services/pull/merge.go index 6a0b64bd9c190..32fcbbfba6f4c 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -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 } diff --git a/services/pull/update.go b/services/pull/update.go index f7b9495a4e105..8fa8656047b5f 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -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 } @@ -117,54 +117,46 @@ 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 } @@ -172,6 +164,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, mergeAllowed = mergeAllowed || mergeAllowedMaintainer } + // if merge is not allowed, rebase is also not allowed + rebaseAllowed = rebaseAllowed && mergeAllowed + return mergeAllowed, rebaseAllowed, nil } diff --git a/services/release/release.go b/services/release/release.go index 3c5392c993897..dd7b73225351d 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -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 } diff --git a/tests/integration/api_auth_test.go b/tests/integration/api_auth_test.go new file mode 100644 index 0000000000000..a6ff6a651999c --- /dev/null +++ b/tests/integration/api_auth_test.go @@ -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"`) +} diff --git a/tests/integration/api_issue_dependency_test.go b/tests/integration/api_issue_dependency_test.go new file mode 100644 index 0000000000000..8356d6058dbea --- /dev/null +++ b/tests/integration/api_issue_dependency_test.go @@ -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, + }) +} diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index 53d37df8e05cc..b3b30a33d5ebf 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -15,6 +15,8 @@ import ( "testing" 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" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -269,6 +271,42 @@ func TestAPIGetReleaseByTag(t *testing.T) { 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) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 1478cb9bfff9a..deacd68a49427 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -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) { defer tests.PrepareTestEnv(t)() const keyword = "test" diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 5896a97ef1c75..d6974aca242c8 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -271,8 +271,8 @@ type RequestWrapper struct { *http.Request } -func (req *RequestWrapper) AddBasicAuth(username string) *RequestWrapper { - req.Request.SetBasicAuth(username, userPassword) +func (req *RequestWrapper) AddBasicAuth(username string, password ...string) *RequestWrapper { + req.Request.SetBasicAuth(username, util.OptionalArg(password, userPassword)) return req } diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index f4565d8c9c6a2..d9dd69cf444c5 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -11,7 +11,11 @@ import ( "time" auth_model "code.gitea.io/gitea/models/auth" + git_model "code.gitea.io/gitea/models/git" 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" user_model "code.gitea.io/gitea/models/user" pull_service "code.gitea.io/gitea/services/pull" @@ -50,6 +54,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) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { // Create PR to test @@ -65,10 +77,32 @@ func TestAPIPullUpdateByRebase(t *testing.T) { 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())) + + // 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) // Test GetDiverging after update @@ -79,6 +113,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 { baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{ Name: "repo-pr-update", diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go index 98581cb6da6b5..79c3f81863932 100644 --- a/tests/integration/repo_tag_test.go +++ b/tests/integration/repo_tag_test.go @@ -147,12 +147,18 @@ func TestRepushTag(t *testing.T) { // delete the tag _, _, err = gitcmd.NewCommand("push", "origin", "--delete", "v2.0").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath}) 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")) + 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) var respRelease *api.Release DecodeJSON(t, resp, &respRelease) assert.True(t, respRelease.IsDraft) + // re-push the tag _, _, err = gitcmd.NewCommand("push", "origin", "--tags", "v2.0").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath}) assert.NoError(t, err)