Skip to content

Commit 028022a

Browse files
committed
Make admins adhere to branch protection rules
This introduces a new flag `BlockAdminMergeOverride` on the branch protection rules that prevents admins/repo owners from bypassing branch protection rules and merging without approvals or failing status checks. Fixes #17131
1 parent 9116665 commit 028022a

File tree

11 files changed

+96
-5
lines changed

11 files changed

+96
-5
lines changed

models/git/protected_branch.go

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type ProtectedBranch struct {
6363
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
6464
ProtectedFilePatterns string `xorm:"TEXT"`
6565
UnprotectedFilePatterns string `xorm:"TEXT"`
66+
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
6667

6768
CreatedUnix timeutil.TimeStamp `xorm:"created"`
6869
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`

modules/structs/repo_branch.go

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type BranchProtection struct {
5252
RequireSignedCommits bool `json:"require_signed_commits"`
5353
ProtectedFilePatterns string `json:"protected_file_patterns"`
5454
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
55+
BlockAdminMergeOverride bool `json:"block_admin_merge_override"`
5556
// swagger:strfmt date-time
5657
Created time.Time `json:"created_at"`
5758
// swagger:strfmt date-time
@@ -90,6 +91,7 @@ type CreateBranchProtectionOption struct {
9091
RequireSignedCommits bool `json:"require_signed_commits"`
9192
ProtectedFilePatterns string `json:"protected_file_patterns"`
9293
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
94+
BlockAdminMergeOverride bool `json:"block_admin_merge_override"`
9395
}
9496

9597
// EditBranchProtectionOption options for editing a branch protection
@@ -121,4 +123,5 @@ type EditBranchProtectionOption struct {
121123
RequireSignedCommits *bool `json:"require_signed_commits"`
122124
ProtectedFilePatterns *string `json:"protected_file_patterns"`
123125
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
126+
BlockAdminMergeOverride *bool `json:"block_admin_merge_override"`
124127
}

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -2461,6 +2461,8 @@ settings.block_on_official_review_requests = Block merge on official review requ
24612461
settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals.
24622462
settings.block_outdated_branch = Block merge if pull request is outdated
24632463
settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch.
2464+
settings.block_admin_merge_override=Administrators must follow branch protection rules
2465+
settings.block_admin_merge_override_desc=Administrators must follow branch protection rules and can not circumvent it.
24642466
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
24652467
settings.merge_style_desc = Merge Styles
24662468
settings.default_merge_style_desc = Default Merge Style

routers/web/repo/issue.go

+1
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,7 @@ func ViewIssue(ctx *context.Context) {
19431943
ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
19441944
ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
19451945
ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist
1946+
ctx.Data["BlockAdminMergeOverride"] = pb.BlockAdminMergeOverride
19461947
}
19471948
ctx.Data["WillSign"] = false
19481949
if ctx.Doer != nil {

routers/web/repo/setting/protected_branch.go

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
256256
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
257257
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
258258
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
259+
protectBranch.BlockAdminMergeOverride = f.BlockAdminMergeOverride
259260

260261
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
261262
UserIDs: whitelistUsers,

services/forms/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ type ProtectBranchForm struct {
219219
RequireSignedCommits bool
220220
ProtectedFilePatterns string
221221
UnprotectedFilePatterns string
222+
BlockAdminMergeOverride bool
222223
}
223224

224225
// Validate validates the fields

services/pull/check.go

+18-4
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,27 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
118118
err = nil
119119
}
120120

121-
// * if the doer is admin, they could skip the branch protection check
121+
// * if the doer is admin, they could sometimes skip the branch protection check
122122
if adminSkipProtectionCheck {
123-
if isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); errCheckAdmin != nil {
123+
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
124+
125+
if err != nil {
126+
return fmt.Errorf("LoadProtectedBranch: %v", err)
127+
}
128+
129+
isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer)
130+
131+
/**
132+
* Checks are only skipable if there is no branch protection available or BlockAdminMergeOverride
133+
* of branch protection is set to false
134+
*/
135+
if errCheckAdmin != nil {
124136
log.Error("Unable to check if %-v is a repo admin in %-v: %v", doer, pr.BaseRepo, errCheckAdmin)
125137
return errCheckAdmin
126-
} else if isRepoAdmin {
127-
err = nil // repo admin can skip the check, so clear the error
138+
} else if isRepoAdmin && pb != nil && !pb.BlockAdminMergeOverride {
139+
err = nil
140+
} else if isRepoAdmin && pb == nil {
141+
err = nil
128142
}
129143
}
130144

templates/repo/issue/view_content/pull.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
165165

166166
{{/* admin can merge without checks, writer can merge when checks succeed */}}
167-
{{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
167+
{{$canMergeNow := and (or (and (not $.BlockAdminMergeOverride) $.IsRepoAdmin) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
168168
{{/* admin and writer both can make an auto merge schedule */}}
169169

170170
{{if $canMergeNow}}

templates/repo/settings/protected_branch.tmpl

+7
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@
323323
<p class="help">{{ctx.Locale.Tr "repo.settings.block_outdated_branch_desc"}}</p>
324324
</div>
325325
</div>
326+
<div class="field">
327+
<div class="ui checkbox">
328+
<input name="block_admin_merge_override" type="checkbox" {{if .Rule.BlockAdminMergeOverride}}checked{{end}}>
329+
<label>{{ctx.Locale.Tr "repo.settings.block_admin_merge_override"}}</label>
330+
<p class="help">{{ctx.Locale.Tr "repo.settings.block_admin_merge_override_desc"}}</p>
331+
</div>
332+
</div>
326333
<div class="divider"></div>
327334

328335
<div class="field">

templates/swagger/v1_json.tmpl

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/pull_merge_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -976,3 +976,52 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.
976976
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
977977
})
978978
}
979+
980+
func TestPullNonMergeForAdminWithBranchProtection(t *testing.T) {
981+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
982+
// create a pull request
983+
session := loginUser(t, "user1")
984+
// user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
985+
forkedName := "repo1-1"
986+
testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
987+
defer func() {
988+
testDeleteRepository(t, session, "user1", forkedName)
989+
}()
990+
testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
991+
testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
992+
993+
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
994+
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
995+
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
996+
BaseRepoID: baseRepo.ID,
997+
BaseBranch: "master",
998+
HeadRepoID: forkedRepo.ID,
999+
HeadBranch: "master",
1000+
})
1001+
1002+
// add protected branch for commit status
1003+
csrf := GetUserCSRFToken(t, session)
1004+
// Change master branch to protected
1005+
pbCreateReq := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
1006+
"_csrf": csrf,
1007+
"rule_name": "master",
1008+
"enable_push": "true",
1009+
"enable_status_check": "true",
1010+
"status_check_contexts": "gitea/actions",
1011+
"block_admin_merge_override": "true",
1012+
})
1013+
session.MakeRequest(t, pbCreateReq, http.StatusSeeOther)
1014+
1015+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
1016+
1017+
mergeReq := NewRequestWithValues(t, "POST", "/api/v1/repos/user2/repo1/pulls/6/merge", map[string]string{
1018+
"_csrf": csrf,
1019+
"head_commit_id": "",
1020+
"merge_when_checks_succeed": "false",
1021+
"force_merge": "true",
1022+
"do": "rebase",
1023+
}).AddTokenAuth(token)
1024+
1025+
session.MakeRequest(t, mergeReq, http.StatusMethodNotAllowed)
1026+
})
1027+
}

0 commit comments

Comments
 (0)