Skip to content

Commit 2483a93

Browse files
authored
Only allow admins to rename default/protected branches (#33276)
Currently, anyone with write permissions to a repo are able to rename default or protected branches. This change follows [GitHub's](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/renaming-a-branch) design by only allowing repo/site admins to change these branches. However, it also follows are current design for protected branches and only allows admins to modify branch names == branch protection rule names. Glob-based rules cannot be renamed by anyone (as was already the case, but we now catch `ErrBranchIsProtected` which we previously did not catch, throwing a 500).
1 parent 4b21a6c commit 2483a93

File tree

7 files changed

+90
-10
lines changed

7 files changed

+90
-10
lines changed

models/fixtures/access.yml

+6
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,9 @@
171171
user_id: 40
172172
repo_id: 61
173173
mode: 4
174+
175+
-
176+
id: 30
177+
user_id: 40
178+
repo_id: 1
179+
mode: 2

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -2714,6 +2714,8 @@ branch.create_branch_operation = Create branch
27142714
branch.new_branch = Create new branch
27152715
branch.new_branch_from = Create new branch from "%s"
27162716
branch.renamed = Branch %s was renamed to %s.
2717+
branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches.
2718+
branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules.
27172719
27182720
tag.create_tag = Create tag %s
27192721
tag.create_tag_operation = Create tag

routers/api/v1/repo/branch.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/models/db"
1313
git_model "code.gitea.io/gitea/models/git"
1414
"code.gitea.io/gitea/models/organization"
15+
repo_model "code.gitea.io/gitea/models/repo"
1516
user_model "code.gitea.io/gitea/models/user"
1617
"code.gitea.io/gitea/modules/git"
1718
"code.gitea.io/gitea/modules/gitrepo"
@@ -443,7 +444,14 @@ func UpdateBranch(ctx *context.APIContext) {
443444

444445
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
445446
if err != nil {
446-
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
447+
switch {
448+
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
449+
ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.")
450+
case errors.Is(err, git_model.ErrBranchIsProtected):
451+
ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.")
452+
default:
453+
ctx.Error(http.StatusInternalServerError, "RenameBranch", err)
454+
}
447455
return
448456
}
449457
if msg == "target_exist" {

routers/web/repo/setting/protected_branch.go

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package setting
55

66
import (
7+
"errors"
78
"fmt"
89
"net/http"
910
"net/url"
@@ -14,6 +15,7 @@ import (
1415
"code.gitea.io/gitea/models/organization"
1516
"code.gitea.io/gitea/models/perm"
1617
access_model "code.gitea.io/gitea/models/perm/access"
18+
repo_model "code.gitea.io/gitea/models/repo"
1719
"code.gitea.io/gitea/modules/base"
1820
"code.gitea.io/gitea/modules/templates"
1921
"code.gitea.io/gitea/modules/web"
@@ -351,9 +353,15 @@ func RenameBranchPost(ctx *context.Context) {
351353
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
352354
if err != nil {
353355
switch {
356+
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
357+
ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error"))
358+
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
354359
case git_model.IsErrBranchAlreadyExists(err):
355360
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
356361
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
362+
case errors.Is(err, git_model.ErrBranchIsProtected):
363+
ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed"))
364+
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
357365
default:
358366
ctx.ServerError("RenameBranch", err)
359367
}

services/repository/branch.go

+23
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,29 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
416416
return "from_not_exist", nil
417417
}
418418

419+
perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
420+
if err != nil {
421+
return "", err
422+
}
423+
424+
isDefault := from == repo.DefaultBranch
425+
if isDefault && !perm.IsAdmin() {
426+
return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
427+
UserID: doer.ID,
428+
RepoName: repo.LowerName,
429+
}
430+
}
431+
432+
// If from == rule name, admins are allowed to modify them.
433+
if protectedBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, from); err != nil {
434+
return "", err
435+
} else if protectedBranch != nil && !perm.IsAdmin() {
436+
return "", repo_model.ErrUserDoesNotHaveAccessToRepo{
437+
UserID: doer.ID,
438+
RepoName: repo.LowerName,
439+
}
440+
}
441+
419442
if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
420443
err2 := gitRepo.RenameBranch(from, to)
421444
if err2 != nil {

tests/integration/api_branch_test.go

+41-8
Original file line numberDiff line numberDiff line change
@@ -190,28 +190,61 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
190190
func TestAPIUpdateBranch(t *testing.T) {
191191
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
192192
t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) {
193-
testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound)
193+
testAPIUpdateBranch(t, "user10", "user10", "repo6", "master", "test", http.StatusNotFound)
194194
})
195195
t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) {
196-
resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
196+
resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
197197
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
198198
})
199199
t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) {
200-
resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
200+
resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
201201
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
202202
})
203203
t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) {
204-
resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
204+
resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
205205
assert.Contains(t, resp.Body.String(), "Branch doesn't exist.")
206206
})
207-
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
208-
testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
207+
t.Run("UpdateBranchWithNonAdminDoer", func(t *testing.T) {
208+
// don't allow default branch renaming
209+
resp := testAPIUpdateBranch(t, "user40", "user2", "repo1", "master", "new-branch-name", http.StatusForbidden)
210+
assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
211+
212+
// don't allow protected branch renaming
213+
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
214+
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
215+
BranchName: "protected-branch",
216+
}).AddTokenAuth(token)
217+
MakeRequest(t, req, http.StatusCreated)
218+
testAPICreateBranchProtection(t, "protected-branch", 1, http.StatusCreated)
219+
resp = testAPIUpdateBranch(t, "user40", "user2", "repo1", "protected-branch", "new-branch-name", http.StatusForbidden)
220+
assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
221+
})
222+
t.Run("UpdateBranchWithGlobedBasedProtectionRulesAndAdminAccess", func(t *testing.T) {
223+
// don't allow branch that falls under glob-based protection rules to be renamed
224+
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
225+
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
226+
RuleName: "protected/**",
227+
EnablePush: true,
228+
}).AddTokenAuth(token)
229+
MakeRequest(t, req, http.StatusCreated)
230+
231+
from := "protected/1"
232+
req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
233+
BranchName: from,
234+
}).AddTokenAuth(token)
235+
MakeRequest(t, req, http.StatusCreated)
236+
237+
resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
238+
assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.")
239+
})
240+
t.Run("UpdateBranchNormalScenario", func(t *testing.T) {
241+
testAPIUpdateBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
209242
})
210243
})
211244
}
212245

213-
func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
214-
token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository)
246+
func testAPIUpdateBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
247+
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
215248
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{
216249
Name: to,
217250
}).AddTokenAuth(token)

tests/integration/api_repo_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -735,5 +735,5 @@ func TestAPIRepoGetAssignees(t *testing.T) {
735735
resp := MakeRequest(t, req, http.StatusOK)
736736
var assignees []*api.User
737737
DecodeJSON(t, resp, &assignees)
738-
assert.Len(t, assignees, 1)
738+
assert.Len(t, assignees, 2)
739739
}

0 commit comments

Comments
 (0)