Add protected branch whitelists for merging (#3689)

* Add database migrations for merge whitelist

* Add merge whitelist settings for protected branches

* Add checks for merge whitelists
This commit is contained in:
Chri-s 2018-03-25 12:01:32 +02:00 committed by Lauris BH
parent 04b7fd87b9
commit 9350ba7947
8 changed files with 210 additions and 42 deletions

View file

@ -30,6 +30,9 @@ type ProtectedBranch struct {
EnableWhitelist bool EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix util.TimeStamp `xorm:"created"` CreatedUnix util.TimeStamp `xorm:"created"`
UpdatedUnix util.TimeStamp `xorm:"updated"` UpdatedUnix util.TimeStamp `xorm:"updated"`
} }
@ -61,6 +64,28 @@ func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
return in return in
} }
// CanUserMerge returns if some user could merge a pull request to this protected branch
func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
if !protectBranch.EnableMergeWhitelist {
return true
}
if base.Int64sContains(protectBranch.MergeWhitelistUserIDs, userID) {
return true
}
if len(protectBranch.WhitelistTeamIDs) == 0 {
return false
}
in, err := IsUserInTeams(userID, protectBranch.MergeWhitelistTeamIDs)
if err != nil {
log.Error(1, "IsUserInTeams:", err)
return false
}
return in
}
// GetProtectedBranchByRepoID getting protected branch by repo ID // GetProtectedBranchByRepoID getting protected branch by repo ID
func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) { func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0) protectedBranches := make([]*ProtectedBranch, 0)
@ -97,40 +122,35 @@ func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
// If ID is 0, it creates a new record. Otherwise, updates existing record. // If ID is 0, it creates a new record. Otherwise, updates existing record.
// This function also performs check if whitelist user and team's IDs have been changed // This function also performs check if whitelist user and team's IDs have been changed
// to avoid unnecessary whitelist delete and regenerate. // to avoid unnecessary whitelist delete and regenerate.
func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs []int64) (err error) { func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs, mergeWhitelistUserIDs, mergeWhitelistTeamIDs []int64) (err error) {
if err = repo.GetOwner(); err != nil { if err = repo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err) return fmt.Errorf("GetOwner: %v", err)
} }
hasUsersChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistUserIDs, whitelistUserIDs) whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, whitelistUserIDs)
if hasUsersChanged {
protectBranch.WhitelistUserIDs = make([]int64, 0, len(whitelistUserIDs))
for _, userID := range whitelistUserIDs {
has, err := hasAccess(x, userID, repo, AccessModeWrite)
if err != nil { if err != nil {
return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err) return err
} else if !has {
continue // Drop invalid user ID
} }
protectBranch.WhitelistUserIDs = whitelist
protectBranch.WhitelistUserIDs = append(protectBranch.WhitelistUserIDs, userID) whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, mergeWhitelistUserIDs)
}
}
// if the repo is in an orgniziation
hasTeamsChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
if hasTeamsChanged {
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
if err != nil { if err != nil {
return fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) return err
}
protectBranch.WhitelistTeamIDs = make([]int64, 0, len(teams))
for i := range teams {
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(whitelistTeamIDs, teams[i].ID) {
protectBranch.WhitelistTeamIDs = append(protectBranch.WhitelistTeamIDs, teams[i].ID)
} }
protectBranch.MergeWhitelistUserIDs = whitelist
// if the repo is in an organization
whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
if err != nil {
return err
} }
protectBranch.WhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, mergeWhitelistTeamIDs)
if err != nil {
return err
} }
protectBranch.MergeWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists // Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 { if protectBranch.ID == 0 {
@ -174,6 +194,73 @@ func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool,
return false, nil return false, nil
} }
// IsProtectedBranchForMerging checks if branch is protected for merging
func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *User) (bool, error) {
if doer == nil {
return true, nil
}
protectedBranch := &ProtectedBranch{
RepoID: repo.ID,
BranchName: branchName,
}
has, err := x.Get(protectedBranch)
if err != nil {
return true, err
} else if has {
return !protectedBranch.CanUserMerge(doer.ID), nil
}
return false, nil
}
// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have write access to the repo.
func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
has, err := hasAccess(x, userID, repo, AccessModeWrite)
if err != nil {
return nil, fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
} else if !has {
continue // Drop invalid user ID
}
whitelist = append(whitelist, userID)
}
return
}
// updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
// the teams from newWhitelist which have write access to the repo.
func updateTeamWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasTeamsChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasTeamsChanged {
return currentWhitelist, nil
}
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
}
whitelist = make([]int64, 0, len(teams))
for i := range teams {
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID)
}
}
return
}
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. // DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
protectedBranch := &ProtectedBranch{ protectedBranch := &ProtectedBranch{

View file

@ -170,6 +170,8 @@ var migrations = []Migration{
NewMigration("add closed_unix column for issues", addIssueClosedTime), NewMigration("add closed_unix column for issues", addIssueClosedTime),
// v58 -> v59 // v58 -> v59
NewMigration("add label descriptions", addLabelsDescriptions), NewMigration("add label descriptions", addLabelsDescriptions),
// v59 -> v60
NewMigration("add merge whitelist for protected branches", addProtectedBranchMergeWhitelist),
} }
// Migrate database to current version // Migrate database to current version

24
models/migrations/v59.go Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"github.com/go-xorm/xorm"
)
func addProtectedBranchMergeWhitelist(x *xorm.Engine) error {
type ProtectedBranch struct {
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
}
if err := x.Sync2(new(ProtectedBranch)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
return nil
}

View file

@ -286,7 +286,7 @@ func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
} }
} }
if protected, err := pr.BaseRepo.IsProtectedBranch(pr.BaseBranch, doer); err != nil { if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr.BaseBranch, doer); err != nil {
return fmt.Errorf("IsProtectedBranch: %v", err) return fmt.Errorf("IsProtectedBranch: %v", err)
} else if protected { } else if protected {
return ErrNotAllowedToMerge{ return ErrNotAllowedToMerge{

View file

@ -133,6 +133,9 @@ type ProtectBranchForm struct {
EnableWhitelist bool EnableWhitelist bool
WhitelistUsers string WhitelistUsers string
WhitelistTeams string WhitelistTeams string
EnableMergeWhitelist bool
MergeWhitelistUsers string
MergeWhitelistTeams string
} }
// Validate validates the fields // Validate validates the fields

View file

@ -1028,6 +1028,10 @@ settings.protect_whitelist_users = Users who can push to this branch
settings.protect_whitelist_search_users = Search users settings.protect_whitelist_search_users = Search users
settings.protect_whitelist_teams = Teams whose members can push to this branch. settings.protect_whitelist_teams = Teams whose members can push to this branch.
settings.protect_whitelist_search_teams = Search teams settings.protect_whitelist_search_teams = Search teams
settings.protect_merge_whitelist_committers = Restrict who can merge pull requests to this branch
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.
settings.protect_merge_whitelist_users = Users who can merge pull requests to this branch
settings.protect_merge_whitelist_teams = Teams whose members can merge pull requests to this branch.
settings.add_protected_branch=Enable protection settings.add_protected_branch=Enable protection
settings.delete_protected_branch=Disable protection settings.delete_protected_branch=Disable protection
settings.update_protect_branch_success = Branch %s protect options changed successfully. settings.update_protect_branch_success = Branch %s protect options changed successfully.

View file

@ -123,6 +123,7 @@ func SettingsProtectedBranch(c *context.Context) {
} }
c.Data["Users"] = users c.Data["Users"] = users
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",") c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
if c.Repo.Owner.IsOrganization() { if c.Repo.Owner.IsOrganization() {
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite) teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite)
@ -132,6 +133,7 @@ func SettingsProtectedBranch(c *context.Context) {
} }
c.Data["Teams"] = teams c.Data["Teams"] = teams
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",") c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
} }
c.Data["Branch"] = protectBranch c.Data["Branch"] = protectBranch
@ -166,7 +168,10 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
protectBranch.EnableWhitelist = f.EnableWhitelist protectBranch.EnableWhitelist = f.EnableWhitelist
whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams) protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
mergeWhitelistUsers, _ := base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
mergeWhitelistTeams, _ := base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams)
if err != nil { if err != nil {
ctx.ServerError("UpdateProtectBranch", err) ctx.ServerError("UpdateProtectBranch", err)
return return

View file

@ -60,6 +60,49 @@
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="field">
<div class="ui checkbox">
<input class="enable-whitelist" name="enable_merge_whitelist" type="checkbox" data-target="#merge_whitelist_box" {{if .Branch.EnableMergeWhitelist}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_committers"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.protect_merge_whitelist_committers_desc"}}</p>
</div>
</div>
<div id="merge_whitelist_box" class="fields {{if not .Branch.EnableMergeWhitelist}}disabled{{end}}">
<div class="whitelist field">
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="merge_whitelist_users" value="{{.merge_whitelist_users}}">
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div>
<div class="menu">
{{range .Users}}
<div class="item" data-value="{{.ID}}">
<img class="ui mini image" src="{{.RelAvatarLink}}">
{{.Name}}
</div>
{{end}}
</div>
</div>
</div>
{{if .Owner.IsOrganization}}
<br>
<div class="whitelist field">
<label>{{.i18n.Tr "repo.settings.protect_merge_whitelist_teams"}}</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="merge_whitelist_teams" value="{{.merge_whitelist_teams}}">
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
<div class="menu">
{{range .Teams}}
<div class="item" data-value="{{.ID}}">
<i class="octicon octicon-jersey"></i>
{{.Name}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>