refactor: migrate protected branch fields into one json column and support system users (#39735)

1. migrate protected branch fields into one json column
2. use the dropdown list for push/force-push user selection, and Actions
user can be added into the list now.
This commit is contained in:
wxiaoguang authored and GitHub committed 2026-10-13 00:40:21 +00:00
1 parent f270534e83
commit c6e2513570
25 files changed
+659 -334

No files matched your search

+17
View File
@@ -518,3 +518,20 @@ func ModifyColumn(ctx context.Context, x EngineMigration, tableName string, col
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return db.Iterate(ctx, cond, f)
}
func EngineTransaction(x EngineMigration, f func(Session) error) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
err := f(sess)
if err != nil {
return err
}
return sess.Commit()
}
func IterateTableByColumn[Bean any](ctx context.Context, x EngineMigration, sess Session, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return db.IterateTableByColumn(ctx, x, sess, colName, cond, f)
}
+1
View File
@@ -436,6 +436,7 @@ func prepareMigrationTasks() []*migration {
newMigration(357, "Normalize legacy team authorize values", v29.NormalizeLegacyTeamAuthorize),
newMigration(358, "Track previous remember-me token hashes", v29.AddPreviousAuthTokenHash),
newMigration(359, "Add stage and number columns to action_task_step", v29.AddStageAndNumberToActionTaskStep),
newMigration(360, "Move branch protection settings to JSON", v29.MoveBranchProtectionConfigToJSON),
}
return preparedMigrations
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/json"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/xorm"
)
type protectedBranchConfigV358 struct {
CanPush bool `xorm:"NOT NULL DEFAULT false" json:"can_push,omitzero"`
EnableWhitelist bool `json:"enable_whitelist,omitzero"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT" json:"whitelist_user_ids,omitzero"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT" json:"whitelist_team_ids,omitzero"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false" json:"enable_merge_whitelist,omitzero"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false" json:"-"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT" json:"merge_whitelist_user_ids,omitzero"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT" json:"merge_whitelist_team_ids,omitzero"`
EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false" json:"enable_bypass_allowlist,omitzero"`
BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT" json:"bypass_allowlist_user_ids,omitzero"`
BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT" json:"bypass_allowlist_team_ids,omitzero"`
CanForcePush bool `xorm:"NOT NULL DEFAULT false" json:"can_force_push,omitzero"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false" json:"enable_force_push_allowlist,omitzero"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT" json:"force_push_allowlist_user_ids,omitzero"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT" json:"force_push_allowlist_team_ids,omitzero"`
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false" json:"-"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false" json:"enable_status_check,omitzero"`
StatusCheckContexts []string `xorm:"JSON TEXT" json:"status_check_contexts,omitzero"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false" json:"enable_approvals_whitelist,omitzero"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT" json:"approvals_whitelist_user_ids,omitzero"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT" json:"approvals_whitelist_team_ids,omitzero"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0" json:"required_approvals,omitzero"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false" json:"block_on_rejected_reviews,omitzero"`
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false" json:"block_on_official_review_requests,omitzero"`
BlockOnCodeownerReviews bool `xorm:"NOT NULL DEFAULT false" json:"block_on_codeowner_reviews,omitzero"`
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false" json:"block_on_outdated_branch,omitzero"`
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false" json:"dismiss_stale_approvals,omitzero"`
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false" json:"ignore_stale_approvals,omitzero"`
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false" json:"require_signed_commits,omitzero"`
ProtectedFilePatterns string `xorm:"TEXT" json:"protected_file_patterns,omitzero"`
UnprotectedFilePatterns string `xorm:"TEXT" json:"unprotected_file_patterns,omitzero"`
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false" json:"block_admin_merge_override,omitzero"`
}
type protectedBranchV358 struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
BranchName string `xorm:"UNIQUE(s)"`
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
Config string `xorm:"LONGTEXT"`
CreatedUnix timeutil.TimeStamp
UpdatedUnix timeutil.TimeStamp
}
func (protectedBranchV358) TableName() string { return "protected_branch" }
type intermediateProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
NewConfig string `xorm:"LONGTEXT config"`
OldConfig protectedBranchConfigV358 `xorm:"extends"`
}
func (intermediateProtectedBranch) TableName() string { return "protected_branch" }
func MoveBranchProtectionConfigToJSON(ctx context.Context, x base.EngineMigration) error {
if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(protectedBranchV358)); err != nil {
return err
}
exists, err := x.Dialect().IsColumnExist(x.DB(), ctx, "protected_branch", "can_push")
if err != nil || !exists {
return err
}
err = base.EngineTransaction(x, func(sess base.Session) error {
err := base.IterateTableByColumn(ctx, x, sess, "id", nil, func(ctx context.Context, pb *intermediateProtectedBranch) error {
if pb.NewConfig != "" {
return nil // skip the already migrated fields
}
const deployKeyUserID int64 = -3 // frozen user.DeployKeyUserID
if pb.OldConfig.WhitelistDeployKeys {
pb.OldConfig.WhitelistUserIDs = append(pb.OldConfig.WhitelistUserIDs, deployKeyUserID)
}
if pb.OldConfig.ForcePushAllowlistDeployKeys {
pb.OldConfig.ForcePushAllowlistUserIDs = append(pb.OldConfig.ForcePushAllowlistUserIDs, deployKeyUserID)
}
configJson, err := json.Marshal(pb.OldConfig)
util.MustNoError(err)
_, err = sess.ID(pb.ID).Cols("config").Update(&protectedBranchV358{Config: string(configJson)})
return err
})
if err != nil {
return err
}
legacyTableInfo, err := x.TableInfo(new(protectedBranchConfigV358))
if err != nil {
return err
}
return base.DropTableColumns(sess, "protected_branch", legacyTableInfo.ColumnsSeq()...)
})
if err != nil {
return err
}
// SQLite rebuilds the table when dropping columns, so restore its unique index.
return x.Sync(new(protectedBranchV358))
}
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"fmt"
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/json"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type protectedBranchBeforeV358 struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
BranchName string `xorm:"UNIQUE(s)"`
Priority int64
CreatedUnix timeutil.TimeStamp
UpdatedUnix timeutil.TimeStamp
Config protectedBranchConfigV358 `xorm:"extends"`
}
func (protectedBranchBeforeV358) TableName() string { return "protected_branch" }
func TestMoveBranchProtectionConfigToJSON(t *testing.T) {
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(protectedBranchBeforeV358))
defer deferrable()
if x == nil || t.Failed() {
return
}
const configJSON = `{
"can_push":true,
"enable_whitelist":true,
"whitelist_user_ids":[2,4,-3],
"whitelist_team_ids":[1,2],
"enable_merge_whitelist":true,
"merge_whitelist_user_ids":[3,5],
"merge_whitelist_team_ids":[3,4],
"enable_bypass_allowlist":true,
"bypass_allowlist_user_ids":[6,7],
"bypass_allowlist_team_ids":[5,6],
"can_force_push":true,
"enable_force_push_allowlist":true,
"force_push_allowlist_user_ids":[8,9,-3],
"force_push_allowlist_team_ids":[7,8],
"enable_status_check":true,
"status_check_contexts":["ci/*","lint"],
"enable_approvals_whitelist":true,
"approvals_whitelist_user_ids":[10,11],
"approvals_whitelist_team_ids":[9,10],
"required_approvals":2,
"block_on_rejected_reviews":true,
"block_on_official_review_requests":true,
"block_on_codeowner_reviews":true,
"block_on_outdated_branch":true,
"dismiss_stale_approvals":true,
"ignore_stale_approvals":true,
"require_signed_commits":true,
"protected_file_patterns":"*.go;*.md",
"unprotected_file_patterns":"docs/**",
"block_admin_merge_override":true
}`
var config protectedBranchConfigV358
require.NoError(t, json.Unmarshal([]byte(configJSON), &config))
config.WhitelistUserIDs = []int64{2, 4}
config.ForcePushAllowlistUserIDs = []int64{8, 9}
config.WhitelistDeployKeys = true
config.ForcePushAllowlistDeployKeys = true
for i := range 101 {
branch := protectedBranchBeforeV358{RepoID: 1, BranchName: fmt.Sprintf("release-%d/*", i), Priority: int64(i), CreatedUnix: 123, UpdatedUnix: 456}
if i%2 == 0 {
branch.Config = config
} else if i == 1 {
branch.Config.StatusCheckContexts = []string{}
}
_, err := x.Insert(&branch)
require.NoError(t, err)
}
require.NoError(t, MoveBranchProtectionConfigToJSON(t.Context(), x))
// A retry must preserve the migrated configuration and its indexes.
require.NoError(t, MoveBranchProtectionConfigToJSON(t.Context(), x))
var branches []protectedBranchV358
require.NoError(t, x.Asc("id").Find(&branches))
require.Len(t, branches, 101)
for i, branch := range branches {
assert.EqualValues(t, i+1, branch.ID)
assert.EqualValues(t, 1, branch.RepoID)
assert.Equal(t, fmt.Sprintf("release-%d/*", i), branch.BranchName)
assert.EqualValues(t, i, branch.Priority)
assert.EqualValues(t, 123, branch.CreatedUnix)
assert.EqualValues(t, 456, branch.UpdatedUnix)
if i%2 == 0 {
assert.JSONEq(t, configJSON, branch.Config)
} else if i == 1 {
assert.JSONEq(t, `{"status_check_contexts":[]}`, branch.Config)
} else {
assert.JSONEq(t, "{}", branch.Config)
}
}
tables, err := x.DBMetas()
require.NoError(t, err)
for _, table := range tables {
if table.Name == "protected_branch" {
assert.ElementsMatch(t, []string{"id", "repo_id", "branch_name", "priority", "config", "created_unix", "updated_unix"}, table.ColumnsSeq())
}
}
_, err = x.Insert(&protectedBranchV358{RepoID: 1, BranchName: branches[0].BranchName})
require.Error(t, err, "the repository/rule unique constraint must survive the migration")
}
+9 -5
View File
@@ -13,8 +13,12 @@ import (
"xorm.io/xorm/schemas"
)
func iterateTableByColumn[Bean any](ctx context.Context, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
table, err := xormEngine.TableInfo(new(Bean))
type sqlTableInfoProvider interface {
TableInfo(bean any) (*schemas.Table, error)
}
func IterateTableByColumn[Bean any](ctx context.Context, tableInfo sqlTableInfoProvider, sess SQLSession, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
table, err := tableInfo.TableInfo(new(Bean))
if err != nil {
return err
}
@@ -36,7 +40,7 @@ func iterateTableByColumn[Bean any](ctx context.Context, colName string, cond bu
}
beans := make([]*Bean, 0, batchSize)
query := GetEngine(ctx).Table(table.Name).Asc(colName)
query := sess.Table(table.Name).Asc(colName)
batchCond := cond
if lastColValue != nil {
@@ -68,9 +72,9 @@ func iterateTableByColumn[Bean any](ctx context.Context, colName string, cond bu
}
func IterateByColumn[Bean any](ctx context.Context, colName string, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return iterateTableByColumn(ctx, colName, cond, f)
return IterateTableByColumn(ctx, xormEngine, GetEngine(ctx), colName, cond, f)
}
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
return iterateTableByColumn(ctx, "", cond, f)
return IterateTableByColumn(ctx, xormEngine, GetEngine(ctx), "", cond, f)
}
+60 -57
View File
@@ -28,50 +28,53 @@ var ErrBranchIsProtected = util.ErrorWrap(util.ErrPermissionDenied, "branch is p
// ProtectedBranch struct
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"`
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"`
BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
BlockOnCodeownerReviews bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
ProtectedFilePatterns string `xorm:"TEXT"`
UnprotectedFilePatterns string `xorm:"TEXT"`
BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"`
ProtectedBranchConfig `xorm:"'config' JSON LONGTEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
// ProtectedBranchConfig stores protection settings in a single JSON column.
type ProtectedBranchConfig struct {
CanPush bool `json:"can_push,omitzero"`
EnableWhitelist bool `json:"enable_whitelist,omitzero"`
WhitelistUserIDs []int64 `json:"whitelist_user_ids,omitzero"`
WhitelistTeamIDs []int64 `json:"whitelist_team_ids,omitzero"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitzero"`
MergeWhitelistUserIDs []int64 `json:"merge_whitelist_user_ids,omitzero"`
MergeWhitelistTeamIDs []int64 `json:"merge_whitelist_team_ids,omitzero"`
EnableBypassAllowlist bool `json:"enable_bypass_allowlist,omitzero"`
BypassAllowlistUserIDs []int64 `json:"bypass_allowlist_user_ids,omitzero"`
BypassAllowlistTeamIDs []int64 `json:"bypass_allowlist_team_ids,omitzero"`
CanForcePush bool `json:"can_force_push,omitzero"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist,omitzero"`
ForcePushAllowlistUserIDs []int64 `json:"force_push_allowlist_user_ids,omitzero"`
ForcePushAllowlistTeamIDs []int64 `json:"force_push_allowlist_team_ids,omitzero"`
EnableStatusCheck bool `json:"enable_status_check,omitzero"`
StatusCheckContexts []string `json:"status_check_contexts,omitzero"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitzero"`
ApprovalsWhitelistUserIDs []int64 `json:"approvals_whitelist_user_ids,omitzero"`
ApprovalsWhitelistTeamIDs []int64 `json:"approvals_whitelist_team_ids,omitzero"`
RequiredApprovals int64 `json:"required_approvals,omitzero"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitzero"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitzero"`
BlockOnCodeownerReviews bool `json:"block_on_codeowner_reviews,omitzero"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitzero"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitzero"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitzero"`
RequireSignedCommits bool `json:"require_signed_commits,omitzero"`
ProtectedFilePatterns string `json:"protected_file_patterns,omitzero"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitzero"`
BlockAdminMergeOverride bool `json:"block_admin_merge_override,omitzero"`
}
func init() {
db.RegisterModel(new(ProtectedBranch))
}
@@ -525,13 +528,17 @@ func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, curre
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if userID == user_model.ActionsUserID || userID == user_model.DeployKeyUserID {
whitelist = append(whitelist, userID)
continue
}
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %w", userID, repo.ID, err)
}
perm, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user)
if err != nil {
return nil, fmt.Errorf("GetIndividualUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
return nil, fmt.Errorf("GetIndividualUserRepoPermission [user_id: %d, repo_id: %d]: %w", userID, repo.ID, err)
}
if !perm.CanWrite(unit.TypeCode) {
@@ -589,7 +596,7 @@ func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id
}
// removeIDsFromProtectedBranch is a helper function to remove IDs from protected branch options
func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID, teamID int64, columnNames []string) error {
func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID, teamID int64) error {
lenUserIDs, lenForcePushIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ForcePushAllowlistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
lenTeamIDs, lenForcePushTeamIDs, lenApprovalTeamIDs, lenMergeTeamIDs := len(p.WhitelistTeamIDs), len(p.ForcePushAllowlistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
@@ -615,7 +622,7 @@ func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userI
lenForcePushTeamIDs != len(p.ForcePushAllowlistTeamIDs) ||
lenApprovalTeamIDs != len(p.ApprovalsWhitelistTeamIDs) ||
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs)) {
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(columnNames...).Update(p); err != nil {
if _, err := db.GetEngine(ctx).ID(p.ID).Cols("config").Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %v", err)
}
}
@@ -624,22 +631,18 @@ func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userI
// RemoveUserIDFromProtectedBranch removes all user ids from protected branch options
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
columnNames := []string{
"whitelist_user_i_ds",
"force_push_allowlist_user_i_ds",
"merge_whitelist_user_i_ds",
"approvals_whitelist_user_i_ds",
}
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
return removeIDsFromProtectedBranch(ctx, p, userID, 0)
}
// RemoveTeamIDFromProtectedBranch removes all team ids from protected branch options
func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error {
columnNames := []string{
"whitelist_team_i_ds",
"force_push_allowlist_team_i_ds",
"merge_whitelist_team_i_ds",
"approvals_whitelist_team_i_ds",
}
return removeIDsFromProtectedBranch(ctx, p, 0, teamID, columnNames)
return removeIDsFromProtectedBranch(ctx, p, 0, teamID)
}
func (protectBranch *ProtectedBranch) HasDeployKeyPush() bool {
return slices.Contains(protectBranch.WhitelistUserIDs, user_model.DeployKeyUserID)
}
func (protectBranch *ProtectedBranch) HasDeployKeyForcePush() bool {
return slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user_model.DeployKeyUserID)
}
+26
View File
@@ -12,6 +12,7 @@ import (
user_model "gitea.dev/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBranchRuleMatch(t *testing.T) {
@@ -202,3 +203,28 @@ func TestCanBypassBranchProtection(t *testing.T) {
// User does not bypass when not in allowlisted teams.
testBypass(t, false, pb, user, false)
}
func TestProtectedBranchConfig(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
pb := &ProtectedBranch{
RepoID: 1, RuleName: "config-test",
ProtectedBranchConfig: ProtectedBranchConfig{
CanPush: true, WhitelistUserIDs: []int64{2, 4}, WhitelistTeamIDs: []int64{1, 2}, StatusCheckContexts: []string{},
},
}
require.NoError(t, db.Insert(ctx, pb))
loaded := unittest.AssertExistsAndLoadBean(t, &ProtectedBranch{ID: pb.ID})
assert.Equal(t, pb.ProtectedBranchConfig, loaded.ProtectedBranchConfig)
require.NoError(t, RemoveUserIDFromProtectedBranch(ctx, loaded, 2))
require.NoError(t, RemoveTeamIDFromProtectedBranch(ctx, loaded, 1))
loaded = unittest.AssertExistsAndLoadBean(t, &ProtectedBranch{ID: pb.ID})
assert.Equal(t, []int64{4}, loaded.WhitelistUserIDs)
assert.Equal(t, []int64{2}, loaded.WhitelistTeamIDs)
assert.True(t, loaded.CanPush)
loaded.ProtectedBranchConfig = ProtectedBranchConfig{}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
require.NoError(t, UpdateProtectBranch(ctx, repo, loaded, WhitelistOptions{}))
loaded = unittest.AssertExistsAndLoadBean(t, &ProtectedBranch{ID: pb.ID})
assert.Equal(t, ProtectedBranchConfig{}, loaded.ProtectedBranchConfig)
}
+1 -1
View File
@@ -188,5 +188,5 @@ func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multi
}
// Get the IDs of all assignees
return user_model.GetUserIDsByNames(ctx, requestAssignees, false)
return user_model.GetDoerUserIDsByNames(ctx, requestAssignees)
}
+1 -1
View File
@@ -433,7 +433,7 @@ func TestRecalculateReviewsOfficial(t *testing.T) {
// Once the reviewer is whitelisted, re-evaluating restores the official flag.
rule.ApprovalsWhitelistUserIDs = []int64{2, reviewer.ID}
_, err = db.GetEngine(t.Context()).ID(rule.ID).Cols("approvals_whitelist_user_i_ds").Update(rule)
_, err = db.GetEngine(t.Context()).ID(rule.ID).Cols("config").Update(rule)
assert.NoError(t, err)
assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue))
+48 -18
View File
@@ -560,13 +560,13 @@ func GetUserSalt() (string, error) {
}
type globalVarsStruct struct {
customCharsReplacement *strings.Replacer
removeCharsRE *regexp.Regexp
transformDiacritics transform.Transformer
replaceCharsHyphenRE *regexp.Regexp
emailToReplacer *strings.Replacer
systemUserNewFuncs map[int64]func() *User
systemUserNameIdMap map[string]int64
customCharsReplacement *strings.Replacer
removeCharsRE *regexp.Regexp
transformDiacritics transform.Transformer
replaceCharsHyphenRE *regexp.Regexp
emailToReplacer *strings.Replacer
systemUserNewFuncs map[int64]func() *User
systemUserLowerNameIdMap map[string]int64
}
var globalVars = sync.OnceValue(func() *globalVarsStruct {
@@ -592,11 +592,11 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
userFuncs := []func() *User{NewGhostUser, NewActionsUser, NewDeployKeyUser, NewCliUser, NewAuthSourceUser}
ret.systemUserNewFuncs = map[int64]func() *User{}
ret.systemUserNameIdMap = map[string]int64{}
ret.systemUserLowerNameIdMap = map[string]int64{}
for _, fn := range userFuncs {
u := fn()
ret.systemUserNewFuncs[u.ID] = fn
ret.systemUserNameIdMap[u.LowerName] = u.ID
ret.systemUserLowerNameIdMap[u.LowerName] = u.ID
}
return ret
})
@@ -1148,20 +1148,50 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]
Find(&ous)
}
// GetUserIDsByNames returns a slice of ids corresponds to names.
func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) {
ids := make([]int64, 0, len(names))
func getDoerUserIDsByNames(ctx context.Context, names []string, allowSystemDoer bool) ([]int64, error) {
namesLower := make([]string, 0, len(names))
namesMap := make(map[string]*User, len(names))
for _, name := range names {
u, err := GetUserByName(ctx, name)
if err != nil {
if ignoreNonExistent {
continue
nameLower := strings.ToLower(name)
namesLower = append(namesLower, nameLower)
namesMap[nameLower] = nil
if allowSystemDoer {
if id, ok := globalVars().systemUserLowerNameIdMap[nameLower]; ok && id != GhostUserID {
namesMap[nameLower] = globalVars().systemUserNewFuncs[id]()
}
return nil, err
}
}
var users []*User
err := db.GetEngine(ctx).In("lower_name", namesLower).Where("`type` IN (?,?)", UserTypeIndividual, UserTypeBot).Find(&users)
if err != nil {
return nil, err
}
for _, user := range users {
namesMap[user.LowerName] = user
}
ids := make([]int64, 0, len(names))
var errFinal error
for _, nameLower := range namesLower {
u := namesMap[nameLower]
if u == nil {
errFinal = ErrUserNotExist{Name: nameLower}
continue
}
ids = append(ids, u.ID)
}
return ids, nil
return ids, errFinal
}
func GetDoerUserIDsByNamesWithSystem(ctx context.Context, names []string) ([]int64, error) {
return getDoerUserIDsByNames(ctx, names, true)
}
// GetDoerUserIDsByNames returns a slice of ids corresponds to names.
// It always returns found user IDs even if some names are not found, but returns an error if any name is not found.
func GetDoerUserIDsByNames(ctx context.Context, names []string) ([]int64, error) {
return getDoerUserIDsByNames(ctx, names, false)
}
// GetUsersBySource returns a list of Users for a login source
+8 -4
View File
@@ -73,7 +73,7 @@ func NewActionsUserWithTaskID(id int64) *User {
}
func NewDeployKeyUser() *User {
return newSystemUser(DeployKeyUserID, "(deploy-key)", "Deploy Key")
return newSystemUser(DeployKeyUserID, "gitea:deploy-key", "Deploy Key")
}
func GetDeployKeyUserDeployKeyID(u *User) (int64, bool) {
@@ -94,17 +94,21 @@ func NewDeployKeyUserWithKeyID(id int64) *User {
func NewCliUser() *User {
// for audit log only
return newSystemUser(CliUserID, "(gitea-cli)", "Gitea CLI")
return newSystemUser(CliUserID, "gitea:cli", "Gitea CLI")
}
func NewAuthSourceUser() *User {
// for audit log only
return newSystemUser(AuthSourceUserID, "(gitea-auth-source)", "Gitea Auth Source")
return newSystemUser(AuthSourceUserID, "gitea:auth-source", "Gitea Auth Source")
}
func GetSystemUserByName(name string) *User {
lowerName := strings.ToLower(name)
uid := globalVars().systemUserNameIdMap[lowerName]
uid := globalVars().systemUserLowerNameIdMap[lowerName]
return GetSystemUserByID(uid)
}
func GetSystemUserByID(uid int64) *User {
if fn := globalVars().systemUserNewFuncs[uid]; fn != nil {
return fn()
}
+7 -9
View File
@@ -21,6 +21,7 @@ import (
"gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -363,18 +364,15 @@ func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
}
func TestGetUserIDsByNames(t *testing.T) {
func TestGetDoerUserIDsByNames(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ids, err := user_model.GetDoerUserIDsByNames(t.Context(), []string{"user1", "user2", "none_existing_user"})
assert.ErrorIs(t, err, util.ErrNotExist)
assert.Equal(t, []int64{1, 2}, ids)
// ignore non existing
IDs, err := user_model.GetUserIDsByNames(t.Context(), []string{"user1", "user2", "none_existing_user"}, true)
ids, err = user_model.GetDoerUserIDsByNamesWithSystem(t.Context(), []string{"gitea-actions", "gitea:deploy-key"})
assert.NoError(t, err)
assert.Equal(t, []int64{1, 2}, IDs)
// ignore non existing
IDs, err = user_model.GetUserIDsByNames(t.Context(), []string{"user1", "do_not_exist"}, false)
assert.Error(t, err)
assert.Equal(t, []int64(nil), IDs)
assert.Equal(t, []int64{-2, -3}, ids)
}
func TestGetMaileableUsersByIDs(t *testing.T) {
+99 -150
View File
@@ -7,6 +7,7 @@ package repo
import (
"errors"
"net/http"
"slices"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
@@ -618,6 +619,34 @@ func ListBranchProtections(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiBps)
}
func legacySetDeployKeyAllowlist(userIDs []int64, enabled bool) []int64 {
if !enabled {
return util.SliceRemoveAll(userIDs, user_model.DeployKeyUserID)
}
if !slices.Contains(userIDs, user_model.DeployKeyUserID) {
userIDs = append(userIDs, user_model.DeployKeyUserID)
}
return userIDs
}
func loadBranchProtectionUserIDs(ctx *context.APIContext, names []string, def ...[]int64) []int64 {
if names == nil {
return util.OptionalArg(def)
}
if ctx.Written() {
return nil
}
ids, err := user_model.GetDoerUserIDsByNamesWithSystem(ctx, names)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
}
return ids
}
// CreateBranchProtection creates a branch protection for a repo
func CreateBranchProtection(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/branch_protections repository repoCreateBranchProtection
@@ -680,54 +709,15 @@ func CreateBranchProtection(ctx *context.APIContext) {
requiredApprovals = form.RequiredApprovals
}
whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
whitelistUsers := loadBranchProtectionUserIDs(ctx, form.PushWhitelistUsernames)
forcePushAllowlistUsers := loadBranchProtectionUserIDs(ctx, form.ForcePushAllowlistUsernames)
mergeWhitelistUsers := loadBranchProtectionUserIDs(ctx, form.MergeWhitelistUsernames)
approvalsWhitelistUsers := loadBranchProtectionUserIDs(ctx, form.ApprovalsWhitelistUsernames)
bypassAllowlistUsers := loadBranchProtectionUserIDs(ctx, util.Iif(form.EnableBypassAllowlist, form.BypassAllowlistUsernames, nil))
if ctx.Written() {
return
}
forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
var bypassAllowlistUsers []int64
if form.EnableBypassAllowlist {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
@@ -779,32 +769,40 @@ func CreateBranchProtection(ctx *context.APIContext) {
}
}
if form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys {
whitelistUsers = legacySetDeployKeyAllowlist(whitelistUsers, true)
}
if form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys {
forcePushAllowlistUsers = legacySetDeployKeyAllowlist(forcePushAllowlistUsers, true)
}
protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID,
RuleName: ruleName,
Priority: form.Priority,
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
EnableMergeWhitelist: form.EnableMergeWhitelist,
EnableBypassAllowlist: form.EnableBypassAllowlist,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
RequiredApprovals: requiredApprovals,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnCodeownerReviews: form.BlockOnCodeownerReviews,
DismissStaleApprovals: form.DismissStaleApprovals,
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
RequireSignedCommits: form.RequireSignedCommits,
ProtectedFilePatterns: form.ProtectedFilePatterns,
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
BlockAdminMergeOverride: form.BlockAdminMergeOverride,
ProtectedBranchConfig: git_model.ProtectedBranchConfig{
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
EnableMergeWhitelist: form.EnableMergeWhitelist,
EnableBypassAllowlist: form.EnableBypassAllowlist,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
RequiredApprovals: requiredApprovals,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnCodeownerReviews: form.BlockOnCodeownerReviews,
DismissStaleApprovals: form.DismissStaleApprovals,
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
RequireSignedCommits: form.RequireSignedCommits,
ProtectedFilePatterns: form.ProtectedFilePatterns,
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
BlockAdminMergeOverride: form.BlockAdminMergeOverride,
},
RepoID: ctx.Repo.Repository.ID,
RuleName: ruleName,
Priority: form.Priority,
}
if err := pull_service.CreateOrUpdateProtectedBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
@@ -819,18 +817,17 @@ func CreateBranchProtection(ctx *context.APIContext) {
BypassUserIDs: bypassAllowlistUsers,
BypassTeamIDs: bypassAllowlistTeams,
}); err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
// Reload from db to get all whitelists
bp, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, ruleName)
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorInternal(err)
} else if bp == nil {
ctx.APIErrorNotFound()
return
}
@@ -888,21 +885,23 @@ func EditBranchProtection(ctx *context.APIContext) {
return
}
pushDeployKeys := protectBranch.HasDeployKeyPush()
forcePushDeployKeys := protectBranch.HasDeployKeyForcePush()
if form.EnablePush != nil {
if !*form.EnablePush {
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
pushDeployKeys = false
} else {
protectBranch.CanPush = true
if form.EnablePushWhitelist != nil {
if !*form.EnablePushWhitelist {
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
pushDeployKeys = false
} else {
protectBranch.EnableWhitelist = true
if form.PushWhitelistDeployKeys != nil {
protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys
pushDeployKeys = *form.PushWhitelistDeployKeys
}
}
}
@@ -913,17 +912,17 @@ func EditBranchProtection(ctx *context.APIContext) {
if !*form.EnableForcePush {
protectBranch.CanForcePush = false
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
forcePushDeployKeys = false
} else {
protectBranch.CanForcePush = true
if form.EnableForcePushAllowlist != nil {
if !*form.EnableForcePushAllowlist {
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
forcePushDeployKeys = false
} else {
protectBranch.EnableForcePushAllowlist = true
if form.ForcePushAllowlistDeployKeys != nil {
protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys
forcePushDeployKeys = *form.ForcePushAllowlistDeployKeys
}
}
}
@@ -998,72 +997,23 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
}
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers, bypassAllowlistUsers []int64
if form.PushWhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
whitelistUsers = protectBranch.WhitelistUserIDs
whitelistUsers := loadBranchProtectionUserIDs(ctx, form.PushWhitelistUsernames, protectBranch.WhitelistUserIDs)
forcePushAllowlistUsers := loadBranchProtectionUserIDs(ctx, form.ForcePushAllowlistUsernames, protectBranch.ForcePushAllowlistUserIDs)
mergeWhitelistUsers := loadBranchProtectionUserIDs(ctx, form.MergeWhitelistUsernames, protectBranch.MergeWhitelistUserIDs)
approvalsWhitelistUsers := loadBranchProtectionUserIDs(ctx, form.ApprovalsWhitelistUsernames, protectBranch.ApprovalsWhitelistUserIDs)
bypassAllowlistUsers := loadBranchProtectionUserIDs(ctx, form.BypassAllowlistUsernames, protectBranch.BypassAllowlistUserIDs)
if ctx.Written() {
return
}
if form.ForcePushAllowlistUsernames != nil {
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
forcePushAllowlistUsers = protectBranch.ForcePushAllowlistUserIDs
if form.PushWhitelistUsernames != nil && form.PushWhitelistDeployKeys == nil && slices.Contains(whitelistUsers, user_model.DeployKeyUserID) {
pushDeployKeys = true
}
if form.MergeWhitelistUsernames != nil {
mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs
}
if form.ApprovalsWhitelistUsernames != nil {
approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
}
if form.BypassAllowlistUsernames != nil {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
bypassAllowlistUsers = protectBranch.BypassAllowlistUserIDs
if form.ForcePushAllowlistUsernames != nil && form.ForcePushAllowlistDeployKeys == nil && slices.Contains(forcePushAllowlistUsers, user_model.DeployKeyUserID) {
forcePushDeployKeys = true
}
whitelistUsers = legacySetDeployKeyAllowlist(whitelistUsers, pushDeployKeys)
forcePushAllowlistUsers = legacySetDeployKeyAllowlist(forcePushAllowlistUsers, forcePushDeployKeys)
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
@@ -1151,7 +1101,7 @@ func EditBranchProtection(ctx *context.APIContext) {
BypassTeamIDs: bypassAllowlistTeams,
})
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
@@ -1199,11 +1149,10 @@ func EditBranchProtection(ctx *context.APIContext) {
// Reload from db to ensure get all whitelists
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorInternal(err)
} else if bp == nil {
ctx.APIErrorNotFound()
return
}
+2 -2
View File
@@ -584,8 +584,8 @@ func CreateIssue(ctx *context.APIContext) {
issue.MilestoneID = form.Milestone
assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: %s", err))
} else {
ctx.APIErrorInternal(err)
}
+6 -4
View File
@@ -4,6 +4,7 @@
package repo
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
@@ -11,6 +12,7 @@ import (
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
@@ -203,13 +205,13 @@ func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption
return
}
assigneeIDs, err := user_model.GetUserIDsByNames(ctx, opts.Assignees, false)
assigneeIDs, err := user_model.GetDoerUserIDsByNames(ctx, opts.Assignees)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
} else {
ctx.APIErrorAuto(err)
}
ctx.APIErrorAuto(err)
return
}
+1 -1
View File
@@ -521,7 +521,7 @@ func CreatePullRequest(ctx *context.APIContext) {
// Get all assignee IDs
assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else {
ctx.APIErrorInternal(err)
+9 -8
View File
@@ -14,6 +14,7 @@ import (
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
@@ -435,13 +436,13 @@ func CreateTagProtection(ctx *context.APIContext) {
}
var whitelistUsers, whitelistTeams []int64
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
whitelistUsers, err = user_model.GetDoerUserIDsByNames(ctx, form.WhitelistUsernames)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorInternal(err)
return
}
@@ -557,13 +558,13 @@ func EditTagProtection(ctx *context.APIContext) {
}
if form.WhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
whitelistUsers, err = user_model.GetDoerUserIDsByNames(ctx, form.WhitelistUsernames)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorInternal(err)
return
}
pt.AllowlistUserIDs = whitelistUsers
+3 -13
View File
@@ -11,7 +11,6 @@ import (
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/private"
@@ -220,19 +219,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// 5. Check if the doer is allowed to push (and force-push if the incoming push is a force-push)
var canPush bool
if ctx.opts.UserID == user_model.DeployKeyUserID {
// This flag is only ever true if protectBranch.CanForcePush is true
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushAllowlist || protectBranch.ForcePushAllowlistDeployKeys)
} else {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
}
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer, ctx.Repo.Permission)
} else {
if isForcePush {
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.Doer, ctx.Repo.Permission)
} else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer, ctx.Repo.Permission)
}
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.Doer, ctx.Repo.Permission)
}
// 6. If we're not allowed to push directly
+3 -4
View File
@@ -78,10 +78,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
metaData.LabelsData.SetSelectedLabelNames(template.Labels)
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, true); err == nil {
for _, userID := range userIDs {
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
}
selectedAssigneeIDs, _ := user_model.GetDoerUserIDsByNames(ctx, template.Assignees)
for _, userID := range selectedAssigneeIDs {
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
}
metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
+12 -8
View File
@@ -19,6 +19,7 @@ import (
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/glob"
"gitea.dev/modules/json"
@@ -76,12 +77,21 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["PageIsSettingsBranches"] = true
c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
users, err := access_model.GetUsersWithAnyUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
repoUsers, err := access_model.GetUsersWithAnyUnitAccess(c, c.Repo.Repository, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
c.ServerError("GetUsersWithUnitAccess", err)
return
}
c.Data["Users"] = users
pushUsers := []*user_model.User{user_model.NewActionsUser(), user_model.NewDeployKeyUser()}
pushUsers = append(pushUsers, repoUsers...)
c.Data["PushUsers"] = pushUsers
c.Data["ForcePushUsers"] = pushUsers
c.Data["MergeUsers"] = repoUsers
c.Data["BypassCheckUsers"] = repoUsers
c.Data["ApprovalUsers"] = repoUsers
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
c.Data["force_push_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistUserIDs), ",")
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",")
@@ -173,11 +183,9 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
case "all":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
case "whitelist":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = true
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
}
@@ -187,18 +195,15 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
default:
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
}
switch f.EnableForcePush {
case "all":
protectBranch.CanForcePush = true
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
case "whitelist":
protectBranch.CanForcePush = true
protectBranch.EnableForcePushAllowlist = true
protectBranch.ForcePushAllowlistDeployKeys = f.ForcePushAllowlistDeployKeys
if strings.TrimSpace(f.ForcePushAllowlistUsers) != "" {
forcePushAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistUsers, ","))
}
@@ -208,7 +213,6 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
default:
protectBranch.CanForcePush = false
protectBranch.EnableForcePushAllowlist = false
protectBranch.ForcePushAllowlistDeployKeys = false
}
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
+36 -29
View File
@@ -120,24 +120,31 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName strin
return branch, nil
}
// getWhitelistEntities returns the names of the entities that are in the whitelist
func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, whitelistIDs []int64) []string {
whitelistUserIDsSet := container.SetOf(whitelistIDs...)
whitelistNames := make([]string, 0)
for _, entity := range entities {
switch v := any(entity).(type) {
case *user_model.User:
if whitelistUserIDsSet.Contains(v.ID) {
whitelistNames = append(whitelistNames, v.Name)
}
case *organization.Team:
if whitelistUserIDsSet.Contains(v.ID) {
whitelistNames = append(whitelistNames, v.Name)
}
func getAllowListUserNames(users []*user_model.User, ids []int64) []string {
userIdMap := map[int64]string{}
for _, user := range users {
userIdMap[user.ID] = user.Name
}
names := make([]string, 0, len(ids))
for _, id := range ids {
if name, ok := userIdMap[id]; ok {
names = append(names, name)
} else if u := user_model.GetSystemUserByID(id); u != nil {
names = append(names, u.Name)
}
}
return names
}
return whitelistNames
func getAllowListTeamNames(teams []*organization.Team, ids []int64) []string {
idSet := container.SetOf(ids...)
names := make([]string, 0, len(ids))
for _, team := range teams {
if idSet.Contains(team.ID) {
names = append(names, team.Name)
}
}
return names
}
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
@@ -147,22 +154,22 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
log.Error("GetRepoReaders: %v", err)
}
pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs)
forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs)
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
bypassAllowlistUsernames := getWhitelistEntities(readers, bp.BypassAllowlistUserIDs)
pushWhitelistUsernames := getAllowListUserNames(readers, bp.WhitelistUserIDs)
forcePushAllowlistUsernames := getAllowListUserNames(readers, bp.ForcePushAllowlistUserIDs)
mergeWhitelistUsernames := getAllowListUserNames(readers, bp.MergeWhitelistUserIDs)
approvalsWhitelistUsernames := getAllowListUserNames(readers, bp.ApprovalsWhitelistUserIDs)
bypassAllowlistUsernames := getAllowListUserNames(readers, bp.BypassAllowlistUserIDs)
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs)
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
bypassAllowlistTeams := getWhitelistEntities(teamReaders, bp.BypassAllowlistTeamIDs)
pushWhitelistTeams := getAllowListTeamNames(teamReaders, bp.WhitelistTeamIDs)
forcePushAllowlistTeams := getAllowListTeamNames(teamReaders, bp.ForcePushAllowlistTeamIDs)
mergeWhitelistTeams := getAllowListTeamNames(teamReaders, bp.MergeWhitelistTeamIDs)
approvalsWhitelistTeams := getAllowListTeamNames(teamReaders, bp.ApprovalsWhitelistTeamIDs)
bypassAllowlistTeams := getAllowListTeamNames(teamReaders, bp.BypassAllowlistTeamIDs)
branchName := ""
if !git_model.IsRuleNameSpecial(bp.RuleName) {
@@ -177,12 +184,12 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
EnablePushWhitelist: bp.EnableWhitelist,
PushWhitelistUsernames: pushWhitelistUsernames,
PushWhitelistTeams: pushWhitelistTeams,
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
PushWhitelistDeployKeys: bp.HasDeployKeyPush(),
EnableForcePush: bp.CanForcePush,
EnableForcePushAllowlist: bp.EnableForcePushAllowlist,
ForcePushAllowlistUsernames: forcePushAllowlistUsernames,
ForcePushAllowlistTeams: forcePushAllowlistTeams,
ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys,
ForcePushAllowlistDeployKeys: bp.HasDeployKeyForcePush(),
EnableMergeWhitelist: bp.EnableMergeWhitelist,
MergeWhitelistUsernames: mergeWhitelistUsernames,
MergeWhitelistTeams: mergeWhitelistTeams,
@@ -970,14 +977,14 @@ func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo
log.Error("GetRepoReaders: %v", err)
}
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
whitelistUsernames := getAllowListUserNames(readers, pt.AllowlistUserIDs)
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs)
whitelistTeams := getAllowListTeamNames(teamReaders, pt.AllowlistTeamIDs)
return &api.TagProtection{
ID: pt.ID,
-2
View File
@@ -156,11 +156,9 @@ type ProtectBranchForm struct {
EnablePush string
WhitelistUsers string
WhitelistTeams string
WhitelistDeployKeys bool
EnableForcePush string
ForcePushAllowlistUsers string
ForcePushAllowlistTeams string
ForcePushAllowlistDeployKeys bool
EnableMergeWhitelist bool
MergeWhitelistUsers string
MergeWhitelistTeams string
+5 -17
View File
@@ -53,7 +53,7 @@
<div id="whitelist_box" class="grouped fields {{if not .Rule.EnableWhitelist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_users"}}</label>
{{template "repo/settings/user_dropdown" (dict "Name" "whitelist_users" "Value" .whitelist_users "Users" .Users)}}
{{template "repo/settings/user_dropdown" (dict "Name" "whitelist_users" "Value" .whitelist_users "Users" .PushUsers)}}
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
@@ -61,12 +61,6 @@
{{template "repo/settings/team_dropdown" (dict "Name" "whitelist_teams" "Value" .whitelist_teams "Teams" .Teams)}}
</div>
{{end}}
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="whitelist_deploy_keys" {{if .Rule.WhitelistDeployKeys}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_deploy_keys"}}</label>
</div>
</div>
</div>
</div>
<div class="field">
@@ -102,7 +96,7 @@
<div id="force_push_allowlist_box" class="grouped fields {{if not .Rule.EnableForcePushAllowlist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_users"}}</label>
{{template "repo/settings/user_dropdown" (dict "Name" "force_push_allowlist_users" "Value" .force_push_allowlist_users "Users" .Users)}}
{{template "repo/settings/user_dropdown" (dict "Name" "force_push_allowlist_users" "Value" .force_push_allowlist_users "Users" .ForcePushUsers)}}
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
@@ -110,12 +104,6 @@
{{template "repo/settings/team_dropdown" (dict "Name" "force_push_allowlist_teams" "Value" .force_push_allowlist_teams "Teams" .Teams)}}
</div>
{{end}}
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="force_push_allowlist_deploy_keys" {{if .Rule.ForcePushAllowlistDeployKeys}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_deploy_keys"}}</label>
</div>
</div>
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}</h5>
@@ -135,7 +123,7 @@
<div id="approvals_whitelist_box" class="grouped fields {{if not .Rule.EnableApprovalsWhitelist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
{{template "repo/settings/user_dropdown" (dict "Name" "approvals_whitelist_users" "Value" .approvals_whitelist_users "Users" .Users)}}
{{template "repo/settings/user_dropdown" (dict "Name" "approvals_whitelist_users" "Value" .approvals_whitelist_users "Users" .ApprovalUsers)}}
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
@@ -211,7 +199,7 @@
<div id="merge_whitelist_box" class="grouped fields {{if not .Rule.EnableMergeWhitelist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
{{template "repo/settings/user_dropdown" (dict "Name" "merge_whitelist_users" "Value" .merge_whitelist_users "Users" .Users)}}
{{template "repo/settings/user_dropdown" (dict "Name" "merge_whitelist_users" "Value" .merge_whitelist_users "Users" .MergeUsers)}}
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
@@ -233,7 +221,7 @@
<div id="bypass_allowlist_box" class="grouped fields {{if not .Rule.EnableBypassAllowlist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_bypass_allowlist_users"}}</label>
{{template "repo/settings/user_dropdown" (dict "Name" "bypass_allowlist_users" "Value" .bypass_allowlist_users "Users" .Users)}}
{{template "repo/settings/user_dropdown" (dict "Name" "bypass_allowlist_users" "Value" .bypass_allowlist_users "Users" .BypassCheckUsers)}}
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
+76
View File
@@ -18,6 +18,7 @@ import (
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
@@ -472,3 +473,78 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, branches, 1)
}
func TestAPIBranchProtectionDeployKeyAllowlists(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
path := "/api/v1/repos/user2/repo1/branch_protections"
createProtection := func(t *testing.T, ruleName string, deployKeys bool) *api.BranchProtection {
t.Helper()
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", path, &api.CreateBranchProtectionOption{
RuleName: ruleName, EnablePush: true, EnablePushWhitelist: true, PushWhitelistDeployKeys: deployKeys,
EnableForcePush: true, EnableForcePushAllowlist: true, ForcePushAllowlistDeployKeys: deployKeys,
}).AddTokenAuth(token), http.StatusCreated)
return DecodeJSON(t, resp, &api.BranchProtection{})
}
t.Run("CreateWithLegacyFlags", func(t *testing.T) {
bp := createProtection(t, "legacy-flags", true)
assert.True(t, bp.PushWhitelistDeployKeys)
assert.True(t, bp.ForcePushAllowlistDeployKeys)
})
t.Run("PreserveDeployKeysWhenUpdatingUsernames", func(t *testing.T) {
createProtection(t, "update-usernames", true)
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", path+"/update-usernames", map[string]any{
"push_whitelist_usernames": []string{"user2"}, "force_push_allowlist_usernames": []string{},
}).AddTokenAuth(token), http.StatusOK)
bp := DecodeJSON(t, resp, &api.BranchProtection{})
assert.True(t, bp.PushWhitelistDeployKeys)
assert.True(t, bp.ForcePushAllowlistDeployKeys)
assert.Equal(t, []string{"user2", "gitea:deploy-key"}, bp.PushWhitelistUsernames)
})
t.Run("LegacyFlagsDisableDeployKeys", func(t *testing.T) {
createProtection(t, "disable-deploy-keys", true)
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", path+"/disable-deploy-keys", map[string]any{
"enable_push": true, "enable_push_whitelist": true, "push_whitelist_deploy_keys": false,
"enable_force_push": true, "enable_force_push_allowlist": true, "force_push_allowlist_deploy_keys": false,
"push_whitelist_usernames": []string{"user2", "gitea:deploy-key"}, "force_push_allowlist_usernames": []string{"gitea:deploy-key"},
}).AddTokenAuth(token), http.StatusOK)
bp := DecodeJSON(t, resp, &api.BranchProtection{})
assert.False(t, bp.PushWhitelistDeployKeys)
assert.False(t, bp.ForcePushAllowlistDeployKeys)
assert.Equal(t, []string{"user2"}, bp.PushWhitelistUsernames)
rule, err := git_model.GetProtectedBranchRuleByName(t.Context(), 1, "disable-deploy-keys")
require.NoError(t, err)
require.NotNil(t, rule)
assert.Equal(t, []int64{2}, rule.WhitelistUserIDs)
assert.Empty(t, rule.ForcePushAllowlistUserIDs)
})
t.Run("AddDeployKeysByUsername", func(t *testing.T) {
createProtection(t, "add-deploy-keys", false)
resp := MakeRequest(t, NewRequestWithJSON(t, "PATCH", path+"/add-deploy-keys", map[string]any{
"push_whitelist_usernames": []string{"user2", "gitea:deploy-key"}, "force_push_allowlist_usernames": []string{"gitea:deploy-key"},
}).AddTokenAuth(token), http.StatusOK)
bp := DecodeJSON(t, resp, &api.BranchProtection{})
assert.True(t, bp.PushWhitelistDeployKeys)
assert.True(t, bp.ForcePushAllowlistDeployKeys)
assert.Equal(t, []string{"user2", "gitea:deploy-key"}, bp.PushWhitelistUsernames)
assert.Equal(t, []string{"gitea:deploy-key"}, bp.ForcePushAllowlistUsernames)
})
t.Run("CreateWithDeployKeyUsernames", func(t *testing.T) {
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", path, &api.CreateBranchProtectionOption{
RuleName: "deploy-key-usernames", EnablePush: true, EnablePushWhitelist: true,
PushWhitelistUsernames: []string{"gitea:deploy-key"}, EnableForcePush: true, EnableForcePushAllowlist: true,
ForcePushAllowlistUsernames: []string{"gitea:deploy-key"},
}).AddTokenAuth(token), http.StatusCreated)
bp := DecodeJSON(t, resp, &api.BranchProtection{})
assert.True(t, bp.PushWhitelistDeployKeys)
assert.True(t, bp.ForcePushAllowlistDeployKeys)
assert.Equal(t, []string{"gitea:deploy-key"}, bp.PushWhitelistUsernames)
assert.Equal(t, []string{"gitea:deploy-key"}, bp.ForcePushAllowlistUsernames)
})
}
+1 -1
View File
@@ -537,7 +537,7 @@ func testAPIIssueAssignees(t *testing.T) {
t.Run("OrganizationAssignee", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"org3"}}).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
MakeRequest(t, req, http.StatusUnprocessableEntity)
checkReq := NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "org3")).AddTokenAuth(token)
MakeRequest(t, checkReq, http.StatusBadRequest)