diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 5be35f4b11..2168376243 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -38,10 +38,15 @@ type ProtectedBranch struct { isPlainName bool `xorm:"-"` CanPush bool `xorm:"NOT NULL DEFAULT false"` EnableWhitelist bool - WhitelistUserIDs []int64 `xorm:"JSON TEXT"` - WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` + CanForcePush bool `xorm:"NOT NULL DEFAULT false"` + EnableForcePushWhitelist bool + ForcePushWhitelistUserIDs []int64 `xorm:"JSON TEXT"` + ForcePushWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + ForcePushWhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` - WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` @@ -142,6 +147,33 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use return in } +// CanUserForcePush returns if some user could force push to this protected branch +// Since force-push extends normal push, we also check if user has regular push access +func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool { + if !protectBranch.CanForcePush { + return false + } + + if !protectBranch.EnableForcePushWhitelist { + return protectBranch.CanUserPush(ctx, user) + } + + if base.Int64sContains(protectBranch.ForcePushWhitelistUserIDs, user.ID) { + return protectBranch.CanUserPush(ctx, user) + } + + if len(protectBranch.ForcePushWhitelistTeamIDs) == 0 { + return false + } + + in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ForcePushWhitelistTeamIDs) + if err != nil { + log.Error("IsUserInTeams: %v", err) + return false + } + return in && protectBranch.CanUserPush(ctx, user) +} + // IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool { if !protectBranch.EnableMergeWhitelist { @@ -303,6 +335,9 @@ type WhitelistOptions struct { UserIDs []int64 TeamIDs []int64 + ForcePushUserIDs []int64 + ForcePushTeamIDs []int64 + MergeUserIDs []int64 MergeTeamIDs []int64 @@ -330,6 +365,13 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote } protectBranch.WhitelistUserIDs = whitelist + whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.ForcePushWhitelistUserIDs, opts.ForcePushUserIDs) + log.Info("%v", whitelist, err) + if err != nil { + return err + } + protectBranch.ForcePushWhitelistUserIDs = whitelist + whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs) if err != nil { return err @@ -349,6 +391,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote } protectBranch.WhitelistTeamIDs = whitelist + whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ForcePushWhitelistTeamIDs, opts.ForcePushTeamIDs) + if err != nil { + return err + } + protectBranch.ForcePushWhitelistTeamIDs = whitelist + whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs) if err != nil { return err @@ -474,13 +522,17 @@ func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error { lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs) p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID) + p.ForcePushWhitelistUserIDs = util.SliceRemoveAll(p.ForcePushWhitelistUserIDs, userID) p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID) p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID) - if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) || + if lenIDs != len(p.WhitelistUserIDs) || + lenApprovalIDs != len(p.ForcePushWhitelistUserIDs) || + lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) || lenMergeIDs != len(p.MergeWhitelistUserIDs) { if _, err := db.GetEngine(ctx).ID(p.ID).Cols( "whitelist_user_i_ds", + "force_push_whitelist_user_i_ds", "merge_whitelist_user_i_ds", "approvals_whitelist_user_i_ds", ).Update(p); err != nil { @@ -502,6 +554,7 @@ func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, te lenMergeIDs != len(p.MergeWhitelistTeamIDs) { if _, err := db.GetEngine(ctx).ID(p.ID).Cols( "whitelist_team_i_ds", + "force_push_whitelist_team_i_ds", "merge_whitelist_team_i_ds", "approvals_whitelist_team_i_ds", ).Update(p); err != nil { diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index e9927aec27..a2ac4bf7e2 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -30,6 +30,11 @@ type BranchProtection struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + EnableForcePush bool `json:"enable_force_push"` + EnableForcePushWhitelist bool `json:"enable_force_push_whitelist"` + ForcePushWhitelistUsernames []string `json:"force_push_whitelist_usernames"` + ForcePushWhitelistTeams []string `json:"force_push_whitelist_teams"` + ForcePushWhitelistDeployKeys bool `json:"force_push_whitelist_deploy_keys"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` @@ -62,6 +67,11 @@ type CreateBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + EnableForcePush bool `json:"enable_force_push"` + EnableForcePushWhitelist bool `json:"enable_force_push_whitelist"` + ForcePushWhitelistUsernames []string `json:"force_push_whitelist_usernames"` + ForcePushWhitelistTeams []string `json:"force_push_whitelist_teams"` + ForcePushWhitelistDeployKeys bool `json:"force_push_whitelist_deploy_keys"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` @@ -87,6 +97,11 @@ type EditBranchProtectionOption struct { PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistTeams []string `json:"push_whitelist_teams"` PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"` + EnableForcePush *bool `json:"enable_force_push"` + EnableForcePushWhitelist *bool `json:"enable_force_push_whitelist"` + ForcePushWhitelistUsernames []string `json:"force_push_whitelist_usernames"` + ForcePushWhitelistTeams []string `json:"force_push_whitelist_teams"` + ForcePushWhitelistDeployKeys *bool `json:"force_push_whitelist_deploy_keys"` EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fa7eee9bc5..11d0633451 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2185,6 +2185,7 @@ settings.event_wiki_desc = Wiki page created, renamed, edited or deleted. settings.event_release = Release settings.event_release_desc = Release published, updated or deleted in a repository. settings.event_push = Push +settings.event_force_push = Force Push settings.event_push_desc = Git push to a repository. settings.event_repository = Repository settings.event_repository_desc = Repository created or deleted. @@ -2278,8 +2279,14 @@ settings.protect_this_branch = Enable Branch Protection settings.protect_this_branch_desc = Prevents deletion and restricts Git pushing and merging to the branch. settings.protect_disable_push = Disable Push settings.protect_disable_push_desc = No pushing will be allowed to this branch. +settings.protect_disable_force_push = Disable Force Push +settings.protect_disable_force_push_desc = No force pushing will be allowed to this branch. settings.protect_enable_push = Enable Push settings.protect_enable_push_desc = Anyone with write access will be allowed to push to this branch (but not force push). +settings.protect_enable_force_push_all = Everyone +settings.protect_enable_force_push_all_desc = Anyone with push access will be allowed to force push to this branch. +settings.protect_enable_force_push_whitelist = Whitelist Restricted Force Push +settings.protect_enable_force_push_whitelist_desc = Only whitelisted users or teams with push access will be allowed to force push to this branch. settings.protect_enable_merge = Enable Merge settings.protect_enable_merge_desc = Anyone with write access will be allowed to merge the pull requests into this branch. settings.protect_whitelist_committers = Whitelist Restricted Push @@ -2289,6 +2296,9 @@ settings.protect_whitelist_users = Whitelisted users for pushing: settings.protect_whitelist_search_users = Search users… settings.protect_whitelist_teams = Whitelisted teams for pushing: settings.protect_whitelist_search_teams = Search teams… +settings.protect_force_push_whitelist_users = Whitelisted users for force pushing: +settings.protect_force_push_whitelist_teams = Whitelisted teams for force pushing: +settings.protect_force_push_whitelist_deploy_keys = Whitelist deploy keys with write access to push. settings.protect_merge_whitelist_committers = Enable Merge Whitelist settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch. settings.protect_merge_whitelist_users = Whitelisted users for merging: diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index f2386a607e..531b5d4b69 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -552,6 +552,15 @@ func CreateBranchProtection(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) return } + forcePushWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -570,7 +579,7 @@ func CreateBranchProtection(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) return } - var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + var whitelistTeams, forcePushWhitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 if repo.Owner.IsOrganization() { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { @@ -581,6 +590,15 @@ func CreateBranchProtection(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) return } + forcePushWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { @@ -606,8 +624,11 @@ func CreateBranchProtection(ctx *context.APIContext) { RuleName: ruleName, CanPush: form.EnablePush, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, - EnableMergeWhitelist: form.EnableMergeWhitelist, WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys, + CanForcePush: form.EnablePush && form.EnableForcePush, + EnableForcePushWhitelist: form.EnablePush && form.EnableForcePush && form.EnableForcePushWhitelist, + ForcePushWhitelistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushWhitelist && form.ForcePushWhitelistDeployKeys, + EnableMergeWhitelist: form.EnableMergeWhitelist, EnableStatusCheck: form.EnableStatusCheck, StatusCheckContexts: form.StatusCheckContexts, EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, @@ -624,6 +645,8 @@ func CreateBranchProtection(ctx *context.APIContext) { err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ UserIDs: whitelistUsers, TeamIDs: whitelistTeams, + ForcePushUserIDs: forcePushWhitelistUsers, + ForcePushTeamIDs: forcePushWhitelistTeams, MergeUserIDs: mergeWhitelistUsers, MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, @@ -754,6 +777,27 @@ func EditBranchProtection(ctx *context.APIContext) { } } + if form.EnableForcePush != nil { + if !*form.EnableForcePush { + protectBranch.CanForcePush = false + protectBranch.EnableForcePushWhitelist = false + protectBranch.ForcePushWhitelistDeployKeys = false + } else { + protectBranch.CanForcePush = true + if form.EnableForcePushWhitelist != nil { + if !*form.EnableForcePushWhitelist { + protectBranch.EnableForcePushWhitelist = false + protectBranch.ForcePushWhitelistDeployKeys = false + } else { + protectBranch.EnableForcePushWhitelist = true + if form.ForcePushWhitelistDeployKeys != nil { + protectBranch.ForcePushWhitelistDeployKeys = *form.ForcePushWhitelistDeployKeys + } + } + } + } + } + if form.EnableMergeWhitelist != nil { protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist } @@ -802,7 +846,7 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch } - var whitelistUsers []int64 + var whitelistUsers, forcePushWhitelistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64 if form.PushWhitelistUsernames != nil { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { @@ -816,7 +860,19 @@ func EditBranchProtection(ctx *context.APIContext) { } else { whitelistUsers = protectBranch.WhitelistUserIDs } - var mergeWhitelistUsers []int64 + if form.ForcePushWhitelistUsernames != nil { + forcePushWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushWhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + } else { + forcePushWhitelistUsers = protectBranch.ForcePushWhitelistUserIDs + } if form.MergeWhitelistUsernames != nil { mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { @@ -830,7 +886,6 @@ func EditBranchProtection(ctx *context.APIContext) { } else { mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs } - var approvalsWhitelistUsers []int64 if form.ApprovalsWhitelistUsernames != nil { approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { @@ -845,7 +900,7 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs } - var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + var whitelistTeams, forcePushWhitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 if repo.Owner.IsOrganization() { if form.PushWhitelistTeams != nil { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) @@ -860,6 +915,19 @@ func EditBranchProtection(ctx *context.APIContext) { } else { whitelistTeams = protectBranch.WhitelistTeamIDs } + if form.ForcePushWhitelistTeams != nil { + forcePushWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushWhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } else { + forcePushWhitelistTeams = protectBranch.ForcePushWhitelistTeamIDs + } if form.MergeWhitelistTeams != nil { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { @@ -891,6 +959,8 @@ func EditBranchProtection(ctx *context.APIContext) { err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ UserIDs: whitelistUsers, TeamIDs: whitelistTeams, + ForcePushUserIDs: forcePushWhitelistUsers, + ForcePushTeamIDs: forcePushWhitelistTeams, MergeUserIDs: mergeWhitelistUsers, MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 4399e49851..2dd1889b5d 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -182,7 +182,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r return } - // 2. Disallow force pushes to protected branches + isForcePush := false + + // 2. Disallow force pushes to protected branches if the option is unchecked if git.EmptySHA != oldCommitID { output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env}) if err != nil { @@ -192,12 +194,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r }) return } else if len(output) > 0 { - log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), - }) - return - + if protectBranch.CanForcePush { + isForcePush = true + } else { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + } } } @@ -244,10 +249,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r } } - // 5. Check if the doer is allowed to push + // 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.DeployKeyID != 0 { - canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + // This flag is only ever true if protectBranch.CanForcePush is true + if isForcePush { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushWhitelist || protectBranch.ForcePushWhitelistDeployKeys) + } else { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + } } else { user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) if err != nil { @@ -257,7 +267,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r }) return } - canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user) + if isForcePush { + canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user) + } else { + canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user) + } } // 6. If we're not allowed to push directly @@ -293,6 +307,13 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r } // Or we're simply not able to push to this protected branch + if isForcePush { + log.Warn("Forbidden: User %d is not allowed to force-push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Not allowued to force-push to protected branch %s", branchName), + }) + return + } log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo) ctx.JSON(http.StatusForbidden, private.Response{ UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 73adfec95a..0ef5dc95da 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -77,6 +77,7 @@ func SettingsProtectedBranch(c *context.Context) { } c.Data["Users"] = users c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",") + c.Data["force_push_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ForcePushWhitelistUserIDs), ",") c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",") c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",") c.Data["status_check_contexts"] = strings.Join(rule.StatusCheckContexts, "\n") @@ -91,6 +92,7 @@ func SettingsProtectedBranch(c *context.Context) { } c.Data["Teams"] = teams c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",") + c.Data["force_push_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ForcePushWhitelistTeamIDs), ",") c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",") c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",") } @@ -149,7 +151,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } } - var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 + var whitelistUsers, whitelistTeams, forcePushWhitelistUsers, forcePushWhitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 protectBranch.RuleName = f.RuleName if f.RequiredApprovals < 0 { ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) @@ -178,6 +180,27 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.WhitelistDeployKeys = false } + switch f.EnableForcePush { + case "all": + protectBranch.CanForcePush = true + protectBranch.EnableForcePushWhitelist = false + protectBranch.ForcePushWhitelistDeployKeys = false + case "whitelist": + protectBranch.CanForcePush = true + protectBranch.EnableForcePushWhitelist = true + protectBranch.ForcePushWhitelistDeployKeys = f.ForcePushWhitelistDeployKeys + if strings.TrimSpace(f.ForcePushWhitelistUsers) != "" { + forcePushWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushWhitelistUsers, ",")) + } + if strings.TrimSpace(f.ForcePushWhitelistTeams) != "" { + forcePushWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ForcePushWhitelistTeams, ",")) + } + default: + protectBranch.CanForcePush = false + protectBranch.EnableForcePushWhitelist = false + protectBranch.ForcePushWhitelistDeployKeys = false + } + protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist if f.EnableMergeWhitelist { if strings.TrimSpace(f.MergeWhitelistUsers) != "" { @@ -236,6 +259,8 @@ func SettingsProtectedBranchPost(ctx *context.Context) { err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ UserIDs: whitelistUsers, TeamIDs: whitelistTeams, + ForcePushUserIDs: forcePushWhitelistUsers, + ForcePushTeamIDs: forcePushWhitelistTeams, MergeUserIDs: mergeWhitelistUsers, MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, diff --git a/services/convert/convert.go b/services/convert/convert.go index 366782390a..5da1136e68 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -111,6 +111,10 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api if err != nil { log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err) } + forcePushWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.ForcePushWhitelistUserIDs) + if err != nil { + log.Error("GetUserNamesByIDs (ForcePushWhitelistUserIDs): %v", err) + } mergeWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.MergeWhitelistUserIDs) if err != nil { log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err) @@ -123,6 +127,10 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api if err != nil { log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err) } + forcePushWhitelistTeams, err := user_model.GetUserNamesByIDs(ctx, bp.ForcePushWhitelistTeamIDs) + if err != nil { + log.Error("GetUserNamesByIDs (ForcePushWhitelistTeamIDs): %v", err) + } mergeWhitelistTeams, err := organization.GetTeamNamesByID(ctx, bp.MergeWhitelistTeamIDs) if err != nil { log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err) @@ -145,6 +153,11 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api PushWhitelistUsernames: pushWhitelistUsernames, PushWhitelistTeams: pushWhitelistTeams, PushWhitelistDeployKeys: bp.WhitelistDeployKeys, + EnableForcePush: bp.CanForcePush, + EnableForcePushWhitelist: bp.EnableForcePushWhitelist, + ForcePushWhitelistUsernames: forcePushWhitelistUsernames, + ForcePushWhitelistTeams: forcePushWhitelistTeams, + ForcePushWhitelistDeployKeys: bp.ForcePushWhitelistDeployKeys, EnableMergeWhitelist: bp.EnableMergeWhitelist, MergeWhitelistUsernames: mergeWhitelistUsernames, MergeWhitelistTeams: mergeWhitelistTeams, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 5df7ec8fd6..6781542fed 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -198,6 +198,10 @@ type ProtectBranchForm struct { WhitelistUsers string WhitelistTeams string WhitelistDeployKeys bool + EnableForcePush string + ForcePushWhitelistUsers string + ForcePushWhitelistTeams string + ForcePushWhitelistDeployKeys bool EnableMergeWhitelist bool MergeWhitelistUsers string MergeWhitelistTeams string diff --git a/services/pull/update.go b/services/pull/update.go index bc8c4a25e5..bf3e3f2d3a 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -116,27 +116,25 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, return false, false, err } - // can't do rebase on protected branch because need force push - if pb == nil { - if err := pr.LoadBaseRepo(ctx); err != nil { - return false, false, err + if err := pr.LoadBaseRepo(ctx); err != nil { + return false, false, err + } + prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) + if err != nil { + if repo_model.IsErrUnitTypeNotExist(err) { + return false, false, nil } - prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) - if err != nil { - if repo_model.IsErrUnitTypeNotExist(err) { - return false, false, nil - } - log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) - return false, false, err - } - rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate + log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) + return false, false, err } - // Update function need push permission + rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate + + // If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push) if pb != nil { pb.Repo = pull.BaseRepo - if !pb.CanUserPush(ctx, user) { - return false, false, nil + if !pb.CanUserForcePush(ctx, user) { + rebaseAllowed = false } } diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index a1e45d805c..1e41cc6442 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -94,6 +94,69 @@

{{ctx.Locale.Tr "repo.settings.require_signed_commits_desc"}}

+
{{ctx.Locale.Tr "repo.settings.event_force_push"}}
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.protect_disable_force_push_desc"}}

+
+
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_all_desc"}}

+
+
+
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_whitelist_desc"}}

+
+
+
+
+ + +
+ {{if .Owner.IsOrganization}} +
+ + +
+ {{end}} +
+
+ + +
+
+
+
{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d32684c1af..1845b84f94 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -16926,6 +16926,14 @@ "type": "boolean", "x-go-name": "EnablePushWhitelist" }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_whitelist": { + "type": "boolean", + "x-go-name": "EnableForcePushWhitelist" + }, "enable_status_check": { "type": "boolean", "x-go-name": "EnableStatusCheck" @@ -16966,6 +16974,24 @@ }, "x-go-name": "PushWhitelistUsernames" }, + "force_push_whitelist_deploy_keys": { + "type": "boolean", + "x-go-name": "ForcePushWhitelistDeployKeys" + }, + "force_push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistTeams" + }, + "force_push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistUsernames" + }, "require_signed_commits": { "type": "boolean", "x-go-name": "RequireSignedCommits" @@ -17567,6 +17593,14 @@ "type": "boolean", "x-go-name": "EnablePushWhitelist" }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_whitelist": { + "type": "boolean", + "x-go-name": "EnableForcePushWhitelist" + }, "enable_status_check": { "type": "boolean", "x-go-name": "EnableStatusCheck" @@ -17607,6 +17641,24 @@ }, "x-go-name": "PushWhitelistUsernames" }, + "force_push_whitelist_deploy_keys": { + "type": "boolean", + "x-go-name": "ForcePushWhitelistDeployKeys" + }, + "force_push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistTeams" + }, + "force_push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistUsernames" + }, "require_signed_commits": { "type": "boolean", "x-go-name": "RequireSignedCommits" @@ -18699,6 +18751,14 @@ "type": "boolean", "x-go-name": "EnablePushWhitelist" }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_whitelist": { + "type": "boolean", + "x-go-name": "EnableForcePushWhitelist" + }, "enable_status_check": { "type": "boolean", "x-go-name": "EnableStatusCheck" @@ -18739,6 +18799,24 @@ }, "x-go-name": "PushWhitelistUsernames" }, + "force_push_whitelist_deploy_keys": { + "type": "boolean", + "x-go-name": "ForcePushWhitelistDeployKeys" + }, + "force_push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistTeams" + }, + "force_push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushWhitelistUsernames" + }, "require_signed_commits": { "type": "boolean", "x-go-name": "RequireSignedCommits"