mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-18 19:11:06 +00:00
- Introduce a “Bypass Protection Allowlist” on branch rules (users/teams) alongside admins, with BlockAdminMergeOverride still respected. - Surface the allowlist in API (create/edit options, structs) and settings UI; merge box now shows the red button + message for bypass-capable users. - Apply bypass logic to merge checks and pre-receive so allowlisted users can override unmet approvals/status checks/ protected files when force-merging. - Add migration for new columns, locale strings, and unit tests (bypass helper; queue test tweak). <img width="1069" height="218" alt="image" src="https://github.com/user-attachments/assets/0b61bc2a-a27f-47f3-a923-613688008e65" /> Fixes #36476 --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Codex GPT-5.3 <codex@openai.com> Co-authored-by: GPT-5.2 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
205 lines
5.2 KiB
Go
205 lines
5.2 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestBranchRuleMatch(t *testing.T) {
|
|
kases := []struct {
|
|
Rule string
|
|
BranchName string
|
|
ExpectedMatch bool
|
|
}{
|
|
{
|
|
Rule: "release/*",
|
|
BranchName: "release/v1.17",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "release/**/v1.17",
|
|
BranchName: "release/test/v1.17",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "release/**/v1.17",
|
|
BranchName: "release/test/1/v1.17",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "release/*/v1.17",
|
|
BranchName: "release/test/1/v1.17",
|
|
ExpectedMatch: false,
|
|
},
|
|
{
|
|
Rule: "release/v*",
|
|
BranchName: "release/v1.16",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "*",
|
|
BranchName: "release/v1.16",
|
|
ExpectedMatch: false,
|
|
},
|
|
{
|
|
Rule: "**",
|
|
BranchName: "release/v1.16",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "main",
|
|
BranchName: "main",
|
|
ExpectedMatch: true,
|
|
},
|
|
{
|
|
Rule: "master",
|
|
BranchName: "main",
|
|
ExpectedMatch: false,
|
|
},
|
|
}
|
|
|
|
for _, kase := range kases {
|
|
pb := ProtectedBranch{RuleName: kase.Rule}
|
|
var should, infact string
|
|
if !kase.ExpectedMatch {
|
|
should = " not"
|
|
} else {
|
|
infact = " not"
|
|
}
|
|
assert.Equal(t, kase.ExpectedMatch, pb.Match(kase.BranchName),
|
|
"%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestUpdateProtectBranchPriorities(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
|
|
// Create some test protected branches with initial priorities
|
|
protectedBranches := []*ProtectedBranch{
|
|
{
|
|
RepoID: repo.ID,
|
|
RuleName: "master",
|
|
Priority: 1,
|
|
},
|
|
{
|
|
RepoID: repo.ID,
|
|
RuleName: "develop",
|
|
Priority: 2,
|
|
},
|
|
{
|
|
RepoID: repo.ID,
|
|
RuleName: "feature/*",
|
|
Priority: 3,
|
|
},
|
|
}
|
|
|
|
for _, pb := range protectedBranches {
|
|
_, err := db.GetEngine(t.Context()).Insert(pb)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Test updating priorities
|
|
newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID}
|
|
err := UpdateProtectBranchPriorities(t.Context(), repo, newPriorities)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify new priorities
|
|
pbs, err := FindRepoProtectedBranchRules(t.Context(), repo.ID)
|
|
assert.NoError(t, err)
|
|
|
|
expectedPriorities := map[string]int64{
|
|
"feature/*": 1,
|
|
"master": 2,
|
|
"develop": 3,
|
|
}
|
|
|
|
for _, pb := range pbs {
|
|
assert.Equal(t, expectedPriorities[pb.RuleName], pb.Priority)
|
|
}
|
|
}
|
|
|
|
func TestNewProtectBranchPriority(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
|
|
err := UpdateProtectBranch(t.Context(), repo, &ProtectedBranch{
|
|
RepoID: repo.ID,
|
|
RuleName: "branch-1",
|
|
Priority: 1,
|
|
}, WhitelistOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
newPB := &ProtectedBranch{
|
|
RepoID: repo.ID,
|
|
RuleName: "branch-2",
|
|
// Priority intentionally omitted
|
|
}
|
|
|
|
err = UpdateProtectBranch(t.Context(), repo, newPB, WhitelistOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
savedPB2, err := GetFirstMatchProtectedBranchRule(t.Context(), repo.ID, "branch-2")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(2), savedPB2.Priority)
|
|
}
|
|
|
|
func TestCanBypassBranchProtection(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // not in team 1
|
|
teamMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
pb := &ProtectedBranch{
|
|
EnableBypassAllowlist: true,
|
|
BypassAllowlistUserIDs: []int64{user.ID},
|
|
}
|
|
|
|
testBypass := func(t *testing.T, expected bool, pb *ProtectedBranch, doer *user_model.User, isAdmin bool) {
|
|
assert.Equal(t, expected, CanBypassBranchProtection(t.Context(), pb, doer, isAdmin))
|
|
}
|
|
// User bypasses via explicit allowlist.
|
|
testBypass(t, true, pb, user, false)
|
|
|
|
// Non-admin cannot bypass when allowlist is disabled.
|
|
pb.EnableBypassAllowlist = false
|
|
testBypass(t, false, pb, user, false)
|
|
|
|
// Repo admin can bypass independently of allowlist when not blocked.
|
|
testBypass(t, true, pb, user, true)
|
|
|
|
// Admin override block still allows bypass for allowlisted users.
|
|
pb.EnableBypassAllowlist = true
|
|
pb.BlockAdminMergeOverride = true
|
|
testBypass(t, true, pb, user, false)
|
|
|
|
// admin cannot bypass without allowlist membership.
|
|
pb.BypassAllowlistUserIDs = nil
|
|
testBypass(t, false, pb, user, true)
|
|
|
|
// admin can bypass when allowlisted.
|
|
pb.BypassAllowlistUserIDs = []int64{user.ID}
|
|
testBypass(t, true, pb, user, true)
|
|
|
|
// User bypasses via team allowlist membership.
|
|
pb.EnableBypassAllowlist = true
|
|
pb.BlockAdminMergeOverride = false
|
|
pb.BypassAllowlistUserIDs = nil
|
|
pb.BypassAllowlistTeamIDs = []int64{1} // team 1 contains user 2 in test fixtures
|
|
testBypass(t, true, pb, teamMember, false)
|
|
|
|
// User does not bypass when not in allowlisted teams.
|
|
testBypass(t, false, pb, user, false)
|
|
}
|