Skip to content

Commit 9350ba7

Browse files
Chri-slafriks
authored andcommitted
Add protected branch whitelists for merging (gogs#3689)
* Add database migrations for merge whitelist * Add merge whitelist settings for protected branches * Add checks for merge whitelists
1 parent 04b7fd8 commit 9350ba7

File tree

8 files changed

+209
-41
lines changed

8 files changed

+209
-41
lines changed

models/branches.go

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ const (
2323

2424
// ProtectedBranch struct
2525
type ProtectedBranch struct {
26-
ID int64 `xorm:"pk autoincr"`
27-
RepoID int64 `xorm:"UNIQUE(s)"`
28-
BranchName string `xorm:"UNIQUE(s)"`
29-
CanPush bool `xorm:"NOT NULL DEFAULT false"`
30-
EnableWhitelist bool
31-
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
32-
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
33-
CreatedUnix util.TimeStamp `xorm:"created"`
34-
UpdatedUnix util.TimeStamp `xorm:"updated"`
26+
ID int64 `xorm:"pk autoincr"`
27+
RepoID int64 `xorm:"UNIQUE(s)"`
28+
BranchName string `xorm:"UNIQUE(s)"`
29+
CanPush bool `xorm:"NOT NULL DEFAULT false"`
30+
EnableWhitelist bool
31+
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
32+
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
33+
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
34+
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
35+
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
36+
CreatedUnix util.TimeStamp `xorm:"created"`
37+
UpdatedUnix util.TimeStamp `xorm:"updated"`
3538
}
3639

3740
// IsProtected returns if the branch is protected
@@ -61,6 +64,28 @@ func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
6164
return in
6265
}
6366

67+
// CanUserMerge returns if some user could merge a pull request to this protected branch
68+
func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
69+
if !protectBranch.EnableMergeWhitelist {
70+
return true
71+
}
72+
73+
if base.Int64sContains(protectBranch.MergeWhitelistUserIDs, userID) {
74+
return true
75+
}
76+
77+
if len(protectBranch.WhitelistTeamIDs) == 0 {
78+
return false
79+
}
80+
81+
in, err := IsUserInTeams(userID, protectBranch.MergeWhitelistTeamIDs)
82+
if err != nil {
83+
log.Error(1, "IsUserInTeams:", err)
84+
return false
85+
}
86+
return in
87+
}
88+
6489
// GetProtectedBranchByRepoID getting protected branch by repo ID
6590
func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
6691
protectedBranches := make([]*ProtectedBranch, 0)
@@ -97,40 +122,35 @@ func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
97122
// If ID is 0, it creates a new record. Otherwise, updates existing record.
98123
// This function also performs check if whitelist user and team's IDs have been changed
99124
// to avoid unnecessary whitelist delete and regenerate.
100-
func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs []int64) (err error) {
125+
func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs, mergeWhitelistUserIDs, mergeWhitelistTeamIDs []int64) (err error) {
101126
if err = repo.GetOwner(); err != nil {
102127
return fmt.Errorf("GetOwner: %v", err)
103128
}
104129

105-
hasUsersChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistUserIDs, whitelistUserIDs)
106-
if hasUsersChanged {
107-
protectBranch.WhitelistUserIDs = make([]int64, 0, len(whitelistUserIDs))
108-
for _, userID := range whitelistUserIDs {
109-
has, err := hasAccess(x, userID, repo, AccessModeWrite)
110-
if err != nil {
111-
return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
112-
} else if !has {
113-
continue // Drop invalid user ID
114-
}
130+
whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, whitelistUserIDs)
131+
if err != nil {
132+
return err
133+
}
134+
protectBranch.WhitelistUserIDs = whitelist
115135

116-
protectBranch.WhitelistUserIDs = append(protectBranch.WhitelistUserIDs, userID)
117-
}
136+
whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, mergeWhitelistUserIDs)
137+
if err != nil {
138+
return err
118139
}
140+
protectBranch.MergeWhitelistUserIDs = whitelist
119141

120-
// if the repo is in an orgniziation
121-
hasTeamsChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
122-
if hasTeamsChanged {
123-
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
124-
if err != nil {
125-
return fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
126-
}
127-
protectBranch.WhitelistTeamIDs = make([]int64, 0, len(teams))
128-
for i := range teams {
129-
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(whitelistTeamIDs, teams[i].ID) {
130-
protectBranch.WhitelistTeamIDs = append(protectBranch.WhitelistTeamIDs, teams[i].ID)
131-
}
132-
}
142+
// if the repo is in an organization
143+
whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
144+
if err != nil {
145+
return err
146+
}
147+
protectBranch.WhitelistTeamIDs = whitelist
148+
149+
whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, mergeWhitelistTeamIDs)
150+
if err != nil {
151+
return err
133152
}
153+
protectBranch.MergeWhitelistTeamIDs = whitelist
134154

135155
// Make sure protectBranch.ID is not 0 for whitelists
136156
if protectBranch.ID == 0 {
@@ -174,6 +194,73 @@ func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool,
174194
return false, nil
175195
}
176196

197+
// IsProtectedBranchForMerging checks if branch is protected for merging
198+
func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *User) (bool, error) {
199+
if doer == nil {
200+
return true, nil
201+
}
202+
203+
protectedBranch := &ProtectedBranch{
204+
RepoID: repo.ID,
205+
BranchName: branchName,
206+
}
207+
208+
has, err := x.Get(protectedBranch)
209+
if err != nil {
210+
return true, err
211+
} else if has {
212+
return !protectedBranch.CanUserMerge(doer.ID), nil
213+
}
214+
215+
return false, nil
216+
}
217+
218+
// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
219+
// the users from newWhitelist which have write access to the repo.
220+
func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
221+
hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
222+
if !hasUsersChanged {
223+
return currentWhitelist, nil
224+
}
225+
226+
whitelist = make([]int64, 0, len(newWhitelist))
227+
for _, userID := range newWhitelist {
228+
has, err := hasAccess(x, userID, repo, AccessModeWrite)
229+
if err != nil {
230+
return nil, fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
231+
} else if !has {
232+
continue // Drop invalid user ID
233+
}
234+
235+
whitelist = append(whitelist, userID)
236+
}
237+
238+
return
239+
}
240+
241+
// updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
242+
// the teams from newWhitelist which have write access to the repo.
243+
func updateTeamWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
244+
hasTeamsChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
245+
if !hasTeamsChanged {
246+
return currentWhitelist, nil
247+
}
248+
249+
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
250+
if err != nil {
251+
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
252+
}
253+
254+
whitelist = make([]int64, 0, len(teams))
255+
for i := range teams {
256+
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(newWhitelist, teams[i].ID) {
257+
whitelist = append(whitelist, teams[i].ID)
258+
}
259+
}
260+
261+
return
262+
}
263+
177264
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
178265
func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
179266
protectedBranch := &ProtectedBranch{

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ var migrations = []Migration{
170170
NewMigration("add closed_unix column for issues", addIssueClosedTime),
171171
// v58 -> v59
172172
NewMigration("add label descriptions", addLabelsDescriptions),
173+
// v59 -> v60
174+
NewMigration("add merge whitelist for protected branches", addProtectedBranchMergeWhitelist),
173175
}
174176

175177
// Migrate database to current version

models/migrations/v59.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/go-xorm/xorm"
11+
)
12+
13+
func addProtectedBranchMergeWhitelist(x *xorm.Engine) error {
14+
type ProtectedBranch struct {
15+
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
16+
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
17+
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
18+
}
19+
20+
if err := x.Sync2(new(ProtectedBranch)); err != nil {
21+
return fmt.Errorf("Sync2: %v", err)
22+
}
23+
return nil
24+
}

models/pull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
286286
}
287287
}
288288

289-
if protected, err := pr.BaseRepo.IsProtectedBranch(pr.BaseBranch, doer); err != nil {
289+
if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr.BaseBranch, doer); err != nil {
290290
return fmt.Errorf("IsProtectedBranch: %v", err)
291291
} else if protected {
292292
return ErrNotAllowedToMerge{

modules/auth/repo_form.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,13 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
129129

130130
// ProtectBranchForm form for changing protected branch settings
131131
type ProtectBranchForm struct {
132-
Protected bool
133-
EnableWhitelist bool
134-
WhitelistUsers string
135-
WhitelistTeams string
132+
Protected bool
133+
EnableWhitelist bool
134+
WhitelistUsers string
135+
WhitelistTeams string
136+
EnableMergeWhitelist bool
137+
MergeWhitelistUsers string
138+
MergeWhitelistTeams string
136139
}
137140

138141
// Validate validates the fields

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,10 @@ settings.protect_whitelist_users = Users who can push to this branch
10281028
settings.protect_whitelist_search_users = Search users
10291029
settings.protect_whitelist_teams = Teams whose members can push to this branch.
10301030
settings.protect_whitelist_search_teams = Search teams
1031+
settings.protect_merge_whitelist_committers = Restrict who can merge pull requests to this branch
1032+
settings.protect_merge_whitelist_committers_desc = Add users or teams to this branch's merge whitelist. Only whitelisted users can merge pull requests to this branch. If not checked, anyone with write permissions can merge pull requests to this branch.
1033+
settings.protect_merge_whitelist_users = Users who can merge pull requests to this branch
1034+
settings.protect_merge_whitelist_teams = Teams whose members can merge pull requests to this branch.
10311035
settings.add_protected_branch=Enable protection
10321036
settings.delete_protected_branch=Disable protection
10331037
settings.update_protect_branch_success = Branch %s protect options changed successfully.

routers/repo/setting_protected_branch.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func SettingsProtectedBranch(c *context.Context) {
123123
}
124124
c.Data["Users"] = users
125125
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
126+
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
126127

127128
if c.Repo.Owner.IsOrganization() {
128129
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite)
@@ -132,6 +133,7 @@ func SettingsProtectedBranch(c *context.Context) {
132133
}
133134
c.Data["Teams"] = teams
134135
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
136+
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
135137
}
136138

137139
c.Data["Branch"] = protectBranch
@@ -166,7 +168,10 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
166168
protectBranch.EnableWhitelist = f.EnableWhitelist
167169
whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
168170
whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
169-
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams)
171+
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
172+
mergeWhitelistUsers, _ := base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
173+
mergeWhitelistTeams, _ := base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
174+
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams)
170175
if err != nil {
171176
ctx.ServerError("UpdateProtectBranch", err)
172177
return

templates/repo/settings/protected_branch.tmpl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,49 @@
6060
</div>
6161
{{end}}
6262
</div>
63+
64+
<div class="field">
65+
<div class="ui checkbox">
66+
<input class="enable-whitelist" name="enable_merge_whitelist" type="checkbox" data-target="#merge_whitelist_box" {{if .Branch.EnableMergeWhitelist}}checked{{end}}>
67+
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_committers"}}</label>
68+
<p class="help">{{.i18n.Tr "repo.settings.protect_merge_whitelist_committers_desc"}}</p>
69+
</div>
70+
</div>
71+
<div id="merge_whitelist_box" class="fields {{if not .Branch.EnableMergeWhitelist}}disabled{{end}}">
72+
<div class="whitelist field">
73+
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
74+
<div class="ui multiple search selection dropdown">
75+
<input type="hidden" name="merge_whitelist_users" value="{{.merge_whitelist_users}}">
76+
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div>
77+
<div class="menu">
78+
{{range .Users}}
79+
<div class="item" data-value="{{.ID}}">
80+
<img class="ui mini image" src="{{.RelAvatarLink}}">
81+
{{.Name}}
82+
</div>
83+
{{end}}
84+
</div>
85+
</div>
86+
</div>
87+
{{if .Owner.IsOrganization}}
88+
<br>
89+
<div class="whitelist field">
90+
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_teams"}}</label>
91+
<div class="ui multiple search selection dropdown">
92+
<input type="hidden" name="merge_whitelist_teams" value="{{.merge_whitelist_teams}}">
93+
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
94+
<div class="menu">
95+
{{range .Teams}}
96+
<div class="item" data-value="{{.ID}}">
97+
<i class="octicon octicon-jersey"></i>
98+
{{.Name}}
99+
</div>
100+
{{end}}
101+
</div>
102+
</div>
103+
</div>
104+
{{end}}
105+
</div>
63106
</div>
64107

65108
<div class="ui divider"></div>

0 commit comments

Comments
 (0)