release: branch protection delete allowlist (#696) #707

Merged
jmiller merged 7 commits from dev into main 2026-06-28 01:20:32 +00:00
14 changed files with 345 additions and 29 deletions
-10
View File
@@ -1,10 +0,0 @@
# DISABLED — auto-release Step 11 recreates dev from main after every release.
# Cascade-dev is redundant and causes version conflicts when both main and dev
# have different version numbers in templateDetails.xml / manifest.xml.
name: "Cascade Main → Dev (DISABLED)"
on: workflow_dispatch
jobs:
noop:
runs-on: ubuntu-latest
steps:
- run: echo "Cascade disabled — auto-release handles dev recreation"
+1
View File
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
- Branch protection delete allowlist: configurable per-user/team/deploy-key allowlist for deleting protected branches (#696)
- Workflow subdirectory discovery: workflows in subdirectories of `.mokogitea/workflows/` are now auto-discovered (#693)
- API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697)
- Edit API token scopes: PATCH /users/{username}/tokens/{id} API endpoint + web UI edit button (#697)
+71 -4
View File
@@ -51,6 +51,12 @@ type ProtectedBranch struct {
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistActionsUser 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"`
@@ -194,6 +200,46 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
return in && protectBranch.CanUserPush(ctx, user)
}
// CanUserDelete returns if some user could delete this protected branch
func (protectBranch *ProtectedBranch) CanUserDelete(ctx context.Context, user *user_model.User) bool {
if !protectBranch.CanDelete {
return false
}
if user.IsActions() && protectBranch.DeleteAllowlistActionsUser {
return true
}
if !protectBranch.EnableDeleteAllowlist {
if err := protectBranch.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
return false
}
isAdmin, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeAdmin)
if err != nil {
log.Error("HasAccessUnit: %v", err)
return false
}
return isAdmin
}
if slices.Contains(protectBranch.DeleteAllowlistUserIDs, user.ID) {
return true
}
if len(protectBranch.DeleteAllowlistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.DeleteAllowlistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
return false
}
return in
}
// 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 {
// Allow the actions bot user if explicitly whitelisted.
@@ -365,6 +411,9 @@ type WhitelistOptions struct {
ApprovalsUserIDs []int64
ApprovalsTeamIDs []int64
DeleteUserIDs []int64
DeleteTeamIDs []int64
}
// UpdateProtectBranch saves branch protection options of repository.
@@ -430,6 +479,18 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.DeleteAllowlistUserIDs, opts.DeleteUserIDs)
if err != nil {
return err
}
protectBranch.DeleteAllowlistUserIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.DeleteAllowlistTeamIDs, opts.DeleteTeamIDs)
if err != nil {
return err
}
protectBranch.DeleteAllowlistTeamIDs = whitelist
// Looks like it's a new rule
if protectBranch.ID == 0 {
// as it's a new rule and if priority was not set, we need to calc it.
@@ -574,14 +635,15 @@ 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 {
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)
lenUserIDs, lenForcePushIDs, lenApprovalIDs, lenMergeIDs, lenDeleteIDs := len(p.WhitelistUserIDs), len(p.ForcePushAllowlistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs), len(p.DeleteAllowlistUserIDs)
lenTeamIDs, lenForcePushTeamIDs, lenApprovalTeamIDs, lenMergeTeamIDs, lenDeleteTeamIDs := len(p.WhitelistTeamIDs), len(p.ForcePushAllowlistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs), len(p.DeleteAllowlistTeamIDs)
if userID > 0 {
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
p.ForcePushAllowlistUserIDs = util.SliceRemoveAll(p.ForcePushAllowlistUserIDs, userID)
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
p.DeleteAllowlistUserIDs = util.SliceRemoveAll(p.DeleteAllowlistUserIDs, userID)
}
if teamID > 0 {
@@ -589,16 +651,19 @@ func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userI
p.ForcePushAllowlistTeamIDs = util.SliceRemoveAll(p.ForcePushAllowlistTeamIDs, teamID)
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
p.DeleteAllowlistTeamIDs = util.SliceRemoveAll(p.DeleteAllowlistTeamIDs, teamID)
}
if (lenUserIDs != len(p.WhitelistUserIDs) ||
lenForcePushIDs != len(p.ForcePushAllowlistUserIDs) ||
lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
lenMergeIDs != len(p.MergeWhitelistUserIDs)) ||
lenMergeIDs != len(p.MergeWhitelistUserIDs) ||
lenDeleteIDs != len(p.DeleteAllowlistUserIDs)) ||
(lenTeamIDs != len(p.WhitelistTeamIDs) ||
lenForcePushTeamIDs != len(p.ForcePushAllowlistTeamIDs) ||
lenApprovalTeamIDs != len(p.ApprovalsWhitelistTeamIDs) ||
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs)) {
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs) ||
lenDeleteTeamIDs != len(p.DeleteAllowlistTeamIDs)) {
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(columnNames...).Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %v", err)
}
@@ -613,6 +678,7 @@ func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, us
"force_push_allowlist_user_i_ds",
"merge_whitelist_user_i_ds",
"approvals_whitelist_user_i_ds",
"delete_allowlist_user_i_ds",
}
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
}
@@ -624,6 +690,7 @@ func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, te
"force_push_allowlist_team_i_ds",
"merge_whitelist_team_i_ds",
"approvals_whitelist_team_i_ds",
"delete_allowlist_team_i_ds",
}
return removeIDsFromProtectedBranch(ctx, p, 0, teamID, columnNames)
}
+1
View File
@@ -437,6 +437,7 @@ func prepareMigrationTasks() []*migration {
newMigration(357, "Drop display_name from repo manifest and update stream config", v1_27.DropDisplayNameColumns),
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
}
return preparedMigrations
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
func AddDeleteAllowlistToProtectedBranch(x *xorm.Engine) error {
type ProtectedBranch struct {
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
DeleteAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(ProtectedBranch))
}
+21 -3
View File
@@ -48,7 +48,13 @@ type BranchProtection struct {
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistDeployKeys bool `json:"delete_allowlist_deploy_keys"`
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
@@ -93,7 +99,13 @@ type CreateBranchProtectionOption struct {
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistDeployKeys bool `json:"delete_allowlist_deploy_keys"`
DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
@@ -129,7 +141,13 @@ type EditBranchProtectionOption struct {
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"`
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
EnableDelete *bool `json:"enable_delete"`
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
DeleteAllowlistDeployKeys *bool `json:"delete_allowlist_deploy_keys"`
DeleteAllowlistActionsUser *bool `json:"delete_allowlist_actions_user"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
+11
View File
@@ -2439,6 +2439,17 @@
"repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:",
"repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.",
"repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.",
"repo.settings.event_delete": "Branch Deletion",
"repo.settings.protect_disable_delete": "Disable Deletion",
"repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.",
"repo.settings.protect_enable_delete_all": "Enable Deletion",
"repo.settings.protect_enable_delete_all_desc": "Anyone with admin access will be allowed to delete this branch.",
"repo.settings.protect_enable_delete_allowlist": "Allowlist Restricted Deletion",
"repo.settings.protect_enable_delete_allowlist_desc": "Only allowlisted users or teams will be allowed to delete this branch.",
"repo.settings.protect_delete_allowlist_users": "Allowlisted users for deletion:",
"repo.settings.protect_delete_allowlist_teams": "Allowlisted teams for deletion:",
"repo.settings.protect_delete_allowlist_deploy_keys": "Allowlist deploy keys with write access to delete.",
"repo.settings.protect_delete_allowlist_actions_user": "Allowlist actions bot user to delete.",
"repo.settings.protect_merge_whitelist_committers": "Enable Merge Allowlist",
"repo.settings.protect_merge_whitelist_committers_desc": "Allow only allowlisted users or teams to merge pull requests into this branch.",
"repo.settings.protect_merge_whitelist_users": "Allowlisted users for merging:",
+81 -3
View File
@@ -693,6 +693,15 @@ func CreateBranchProtection(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
deleteAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.DeleteAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
@@ -711,7 +720,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var whitelistTeams, forcePushAllowlistTeams, deleteAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
if err != nil {
@@ -731,6 +740,15 @@ func CreateBranchProtection(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
deleteAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.DeleteAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
@@ -763,6 +781,10 @@ func CreateBranchProtection(ctx *context.APIContext) {
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
CanDelete: form.EnableDelete,
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
DeleteAllowlistDeployKeys: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistDeployKeys,
DeleteAllowlistActionsUser: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistActionsUser,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
EnableStatusCheck: form.EnableStatusCheck,
@@ -785,6 +807,8 @@ func CreateBranchProtection(ctx *context.APIContext) {
TeamIDs: whitelistTeams,
ForcePushUserIDs: forcePushAllowlistUsers,
ForcePushTeamIDs: forcePushAllowlistTeams,
DeleteUserIDs: deleteAllowlistUsers,
DeleteTeamIDs: deleteAllowlistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
@@ -911,6 +935,32 @@ func EditBranchProtection(ctx *context.APIContext) {
}
}
if form.EnableDelete != nil {
if !*form.EnableDelete {
protectBranch.CanDelete = false
protectBranch.EnableDeleteAllowlist = false
protectBranch.DeleteAllowlistDeployKeys = false
protectBranch.DeleteAllowlistActionsUser = false
} else {
protectBranch.CanDelete = true
if form.EnableDeleteAllowlist != nil {
if !*form.EnableDeleteAllowlist {
protectBranch.EnableDeleteAllowlist = false
protectBranch.DeleteAllowlistDeployKeys = false
protectBranch.DeleteAllowlistActionsUser = false
} else {
protectBranch.EnableDeleteAllowlist = true
if form.DeleteAllowlistDeployKeys != nil {
protectBranch.DeleteAllowlistDeployKeys = *form.DeleteAllowlistDeployKeys
}
if form.DeleteAllowlistActionsUser != nil {
protectBranch.DeleteAllowlistActionsUser = *form.DeleteAllowlistActionsUser
}
}
}
}
}
if form.Priority != nil {
protectBranch.Priority = *form.Priority
}
@@ -977,7 +1027,7 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
}
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
var whitelistUsers, forcePushAllowlistUsers, deleteAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
if form.PushWhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
@@ -1004,6 +1054,19 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
forcePushAllowlistUsers = protectBranch.ForcePushAllowlistUserIDs
}
if form.DeleteAllowlistUsernames != nil {
deleteAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.DeleteAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
} else {
deleteAllowlistUsers = protectBranch.DeleteAllowlistUserIDs
}
if form.MergeWhitelistUsernames != nil {
mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
@@ -1031,7 +1094,7 @@ func EditBranchProtection(ctx *context.APIContext) {
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var whitelistTeams, forcePushAllowlistTeams, deleteAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
if repo.Owner.IsOrganization() {
if form.PushWhitelistTeams != nil {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
@@ -1059,6 +1122,19 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
forcePushAllowlistTeams = protectBranch.ForcePushAllowlistTeamIDs
}
if form.DeleteAllowlistTeams != nil {
deleteAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.DeleteAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
} else {
deleteAllowlistTeams = protectBranch.DeleteAllowlistTeamIDs
}
if form.MergeWhitelistTeams != nil {
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
@@ -1092,6 +1168,8 @@ func EditBranchProtection(ctx *context.APIContext) {
TeamIDs: whitelistTeams,
ForcePushUserIDs: forcePushAllowlistUsers,
ForcePushTeamIDs: forcePushAllowlistTeams,
DeleteUserIDs: deleteAllowlistUsers,
DeleteTeamIDs: deleteAllowlistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
+22 -5
View File
@@ -182,12 +182,29 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
//
// First of all we need to enforce absolutely:
//
// 1. Detect and prevent deletion of the branch
// 1. Detect and prevent deletion of the branch (unless user is in delete allowlist)
if newCommitID == objectFormat.EmptyObjectID().String() {
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName),
})
canDelete := false
if ctx.opts.DeployKeyID != 0 {
canDelete = protectBranch.CanDelete && (!protectBranch.EnableDeleteAllowlist || protectBranch.DeleteAllowlistDeployKeys)
} else {
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to GetUserByID for delete check in %-v: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to GetUserByID: %v", err),
})
return
}
canDelete = protectBranch.CanUserDelete(ctx, user)
}
if !canDelete {
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName),
})
return
}
return
}
+29 -1
View File
@@ -82,6 +82,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["Users"] = users
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
c.Data["force_push_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistUserIDs), ",")
c.Data["delete_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.DeleteAllowlistUserIDs), ",")
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")
@@ -97,6 +98,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["Teams"] = teams
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",")
c.Data["force_push_allowlist_teams"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistTeamIDs), ",")
c.Data["delete_allowlist_teams"] = strings.Join(base.Int64sToStrings(rule.DeleteAllowlistTeamIDs), ",")
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",")
c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",")
}
@@ -155,7 +157,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
}
}
var whitelistUsers, whitelistTeams, forcePushAllowlistUsers, forcePushAllowlistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
var whitelistUsers, whitelistTeams, forcePushAllowlistUsers, forcePushAllowlistTeams, deleteAllowlistUsers, deleteAllowlistTeams, 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"))
@@ -211,6 +213,30 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.ForcePushAllowlistActionsUser = false
}
switch f.EnableDelete {
case "all":
protectBranch.CanDelete = true
protectBranch.EnableDeleteAllowlist = false
protectBranch.DeleteAllowlistDeployKeys = false
protectBranch.DeleteAllowlistActionsUser = false
case "whitelist":
protectBranch.CanDelete = true
protectBranch.EnableDeleteAllowlist = true
protectBranch.DeleteAllowlistDeployKeys = f.DeleteAllowlistDeployKeys
protectBranch.DeleteAllowlistActionsUser = f.DeleteAllowlistActionsUser
if strings.TrimSpace(f.DeleteAllowlistUsers) != "" {
deleteAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.DeleteAllowlistUsers, ","))
}
if strings.TrimSpace(f.DeleteAllowlistTeams) != "" {
deleteAllowlistTeams, _ = base.StringsToInt64s(strings.Split(f.DeleteAllowlistTeams, ","))
}
default:
protectBranch.CanDelete = false
protectBranch.EnableDeleteAllowlist = false
protectBranch.DeleteAllowlistDeployKeys = false
protectBranch.DeleteAllowlistActionsUser = false
}
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
protectBranch.MergeWhitelistActionsUser = f.EnableMergeWhitelist && f.MergeWhitelistActionsUser
if f.EnableMergeWhitelist {
@@ -274,6 +300,8 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
TeamIDs: whitelistTeams,
ForcePushUserIDs: forcePushAllowlistUsers,
ForcePushTeamIDs: forcePushAllowlistTeams,
DeleteUserIDs: deleteAllowlistUsers,
DeleteTeamIDs: deleteAllowlistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
+8
View File
@@ -146,6 +146,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs)
forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs)
deleteAllowlistUsernames := getWhitelistEntities(readers, bp.DeleteAllowlistUserIDs)
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
@@ -156,6 +157,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs)
deleteAllowlistTeams := getWhitelistEntities(teamReaders, bp.DeleteAllowlistTeamIDs)
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
@@ -180,6 +182,12 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
ForcePushAllowlistTeams: forcePushAllowlistTeams,
ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys,
ForcePushAllowlistActionsUser: bp.ForcePushAllowlistActionsUser,
EnableDelete: bp.CanDelete,
EnableDeleteAllowlist: bp.EnableDeleteAllowlist,
DeleteAllowlistUsernames: deleteAllowlistUsernames,
DeleteAllowlistTeams: deleteAllowlistTeams,
DeleteAllowlistDeployKeys: bp.DeleteAllowlistDeployKeys,
DeleteAllowlistActionsUser: bp.DeleteAllowlistActionsUser,
EnableMergeWhitelist: bp.EnableMergeWhitelist,
MergeWhitelistUsernames: mergeWhitelistUsernames,
MergeWhitelistTeams: mergeWhitelistTeams,
+5
View File
@@ -192,6 +192,11 @@ type ProtectBranchForm struct {
ForcePushAllowlistTeams string
ForcePushAllowlistDeployKeys bool
ForcePushAllowlistActionsUser bool
EnableDelete string
DeleteAllowlistUsers string
DeleteAllowlistTeams string
DeleteAllowlistDeployKeys bool
DeleteAllowlistActionsUser bool
EnableMergeWhitelist bool
MergeWhitelistUsers string
MergeWhitelistTeams string
+6 -3
View File
@@ -563,12 +563,15 @@ func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchNam
return util.NewPermissionDeniedErrorf("permission denied to access repo %d unit %s", repo.ID, unit.TypeCode.LogString())
}
isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName)
protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil {
return err
}
if isProtected {
return git_model.ErrBranchIsProtected
if protectBranch != nil {
protectBranch.Repo = repo
if !protectBranch.CanUserDelete(ctx, doer) {
return git_model.ErrBranchIsProtected
}
}
return nil
}
@@ -172,6 +172,75 @@
</div>
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_delete"}}</h5>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="enable_delete" value="none" class="toggle-target-disabled" data-target="#delete_allowlist_box" {{if not .Rule.CanDelete}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_disable_delete"}}</label>
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_disable_delete_desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="enable_delete" value="all" class="toggle-target-disabled" data-target="#delete_allowlist_box" {{if and (.Rule.CanDelete) (not .Rule.EnableDeleteAllowlist)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_enable_delete_all"}}</label>
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_enable_delete_all_desc"}}</p>
</div>
</div>
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="enable_delete" value="whitelist" class="toggle-target-enabled" data-target="#delete_allowlist_box" {{if and (.Rule.CanDelete) (.Rule.EnableDeleteAllowlist)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_enable_delete_allowlist"}}</label>
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_enable_delete_allowlist_desc"}}</p>
</div>
</div>
<div id="delete_allowlist_box" class="grouped fields {{if not .Rule.EnableDeleteAllowlist}}disabled{{end}}">
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_delete_allowlist_users"}}</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="delete_allowlist_users" value="{{.delete_allowlist_users}}">
<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
<div class="menu">
{{range .Users}}
<div class="item" data-value="{{.ID}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}}{{template "repo/search_name" .}}
</div>
{{end}}
</div>
</div>
</div>
{{if .Owner.IsOrganization}}
<div class="checkbox-sub-item field">
<label>{{ctx.Locale.Tr "repo.settings.protect_delete_allowlist_teams"}}</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="delete_allowlist_teams" value="{{.delete_allowlist_teams}}">
<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
<div class="menu">
{{range .Teams}}
<div class="item" data-value="{{.ID}}">
{{svg "octicon-people"}}
{{.Name}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="delete_allowlist_deploy_keys" {{if .Rule.DeleteAllowlistDeployKeys}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_delete_allowlist_deploy_keys"}}</label>
</div>
</div>
<div class="checkbox-sub-item field">
<div class="ui checkbox">
<input type="checkbox" name="delete_allowlist_actions_user" {{if .Rule.DeleteAllowlistActionsUser}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protect_delete_allowlist_actions_user"}}</label>
</div>
</div>
</div>
</div>
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}</h5>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.protect_required_approvals"}}</label>