feat: Add bypass allowlist for branch protection (#36514)

- 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>
This commit is contained in:
Nicolas
2026-05-16 16:23:42 +02:00
committed by GitHub
parent 54ff68b0a9
commit eb93981d45
23 changed files with 572 additions and 40 deletions
+49
View File
@@ -9,6 +9,7 @@ import (
"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"
)
@@ -153,3 +154,51 @@ func TestNewProtectBranchPriority(t *testing.T) {
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)
}