aboutsummaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorJonas Franz <info@jonasfranz.software>2018-12-11 12:28:37 +0100
committerLunny Xiao <xiaolunwen@gmail.com>2018-12-11 19:28:37 +0800
commit9681c8373426e8364760f033b9935b81b9276351 (patch)
treed9308c33809fdfa7859fd9a1057432a74a28c92f /models
parent64680b72bd1b0a0fc164296ff0cc7319ae7dc4ca (diff)
downloadgitea-9681c8373426e8364760f033b9935b81b9276351.tar.gz
gitea-9681c8373426e8364760f033b9935b81b9276351.zip
Approvals at Branch Protection (#5350)
* Add branch protection for approvals Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add required approvals Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add missing comments and fmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add type = approval and group by reviewer_id to review * Prevent users from adding negative review limits * Add migration for approval whitelists Signed-off-by: Jonas Franz <info@jonasfranz.software>
Diffstat (limited to 'models')
-rw-r--r--models/branches.go104
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v74.go16
-rw-r--r--models/org_team.go8
-rw-r--r--models/org_team_test.go14
-rw-r--r--models/pull.go25
-rw-r--r--models/review.go17
7 files changed, 156 insertions, 30 deletions
diff --git a/models/branches.go b/models/branches.go
index bbcd342baa..f2bf9a80a0 100644
--- a/models/branches.go
+++ b/models/branches.go
@@ -23,18 +23,21 @@ const (
// ProtectedBranch struct
type ProtectedBranch struct {
- ID int64 `xorm:"pk autoincr"`
- RepoID int64 `xorm:"UNIQUE(s)"`
- BranchName string `xorm:"UNIQUE(s)"`
- CanPush bool `xorm:"NOT NULL DEFAULT false"`
- EnableWhitelist bool
- WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
- WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
- EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
- MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
- MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
- CreatedUnix util.TimeStamp `xorm:"created"`
- UpdatedUnix util.TimeStamp `xorm:"updated"`
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"UNIQUE(s)"`
+ BranchName string `xorm:"UNIQUE(s)"`
+ CanPush bool `xorm:"NOT NULL DEFAULT false"`
+ EnableWhitelist bool
+ WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
+ WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
+ EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
+ MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
+ MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
+ ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
+ ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
+ RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
+ CreatedUnix util.TimeStamp `xorm:"created"`
+ UpdatedUnix util.TimeStamp `xorm:"updated"`
}
// IsProtected returns if the branch is protected
@@ -86,6 +89,41 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
return in
}
+// HasEnoughApprovals returns true if pr has enough granted approvals.
+func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
+ if protectBranch.RequiredApprovals == 0 {
+ return true
+ }
+ return protectBranch.GetGrantedApprovalsCount(pr) >= protectBranch.RequiredApprovals
+}
+
+// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
+func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
+ reviews, err := GetReviewersByPullID(pr.ID)
+ if err != nil {
+ log.Error(1, "GetUniqueApprovalsByPullRequestID:", err)
+ return 0
+ }
+ approvals := int64(0)
+ userIDs := make([]int64, 0)
+ for _, review := range reviews {
+ if review.Type != ReviewTypeApprove {
+ continue
+ }
+ if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, review.ID) {
+ approvals++
+ continue
+ }
+ userIDs = append(userIDs, review.ID)
+ }
+ approvalTeamCount, err := UsersInTeamsCount(userIDs, protectBranch.ApprovalsWhitelistTeamIDs)
+ if err != nil {
+ log.Error(1, "UsersInTeamsCount:", err)
+ return 0
+ }
+ return approvalTeamCount + approvals
+}
+
// GetProtectedBranchByRepoID getting protected branch by repo ID
func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0)
@@ -118,40 +156,64 @@ func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
return rel, nil
}
+// WhitelistOptions represent all sorts of whitelists used for protected branches
+type WhitelistOptions struct {
+ UserIDs []int64
+ TeamIDs []int64
+
+ MergeUserIDs []int64
+ MergeTeamIDs []int64
+
+ ApprovalsUserIDs []int64
+ ApprovalsTeamIDs []int64
+}
+
// UpdateProtectBranch saves branch protection options of repository.
// 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
// to avoid unnecessary whitelist delete and regenerate.
-func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs, mergeWhitelistUserIDs, mergeWhitelistTeamIDs []int64) (err error) {
+func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
if err = repo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err)
}
- whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, whitelistUserIDs)
+ whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil {
return err
}
protectBranch.WhitelistUserIDs = whitelist
- whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, mergeWhitelistUserIDs)
+ whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistUserIDs = whitelist
+ whitelist, err = updateUserWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
+ if err != nil {
+ return err
+ }
+ protectBranch.ApprovalsWhitelistUserIDs = whitelist
+
// if the repo is in an organization
- whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
+ whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
return err
}
protectBranch.WhitelistTeamIDs = whitelist
- whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, mergeWhitelistTeamIDs)
+ whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistTeamIDs = whitelist
+ whitelist, err = updateTeamWhitelist(repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
+ if err != nil {
+ return err
+ }
+ protectBranch.ApprovalsWhitelistTeamIDs = whitelist
+
// Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 {
if _, err = x.Insert(protectBranch); err != nil {
@@ -213,7 +275,7 @@ func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User)
}
// IsProtectedBranchForMerging checks if branch is protected for merging
-func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *User) (bool, error) {
+func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName string, doer *User) (bool, error) {
if doer == nil {
return true, nil
}
@@ -227,7 +289,7 @@ func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *Use
if err != nil {
return true, err
} else if has {
- return !protectedBranch.CanUserMerge(doer.ID), nil
+ return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr), nil
}
return false, nil
@@ -270,14 +332,14 @@ func updateTeamWhitelist(repo *Repository, currentWhitelist, newWhitelist []int6
return currentWhitelist, nil
}
- teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
+ teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeRead)
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) {
+ if com.IsSliceContainsInt64(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID)
}
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 6ac5004eb1..6c28989c2e 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -200,6 +200,8 @@ var migrations = []Migration{
NewMigration("add review", addReview),
// v73 -> v74
NewMigration("add must_change_password column for users table", addMustChangePassword),
+ // v74 -> v75
+ NewMigration("add approval whitelists to protected branches", addApprovalWhitelistsToProtectedBranches),
}
// Migrate database to current version
diff --git a/models/migrations/v74.go b/models/migrations/v74.go
new file mode 100644
index 0000000000..66e958c7fa
--- /dev/null
+++ b/models/migrations/v74.go
@@ -0,0 +1,16 @@
+// 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 "github.com/go-xorm/xorm"
+
+func addApprovalWhitelistsToProtectedBranches(x *xorm.Engine) error {
+ type ProtectedBranch struct {
+ ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
+ ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
+ RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
+ }
+ return x.Sync2(new(ProtectedBranch))
+}
diff --git a/models/org_team.go b/models/org_team.go
index cad4af2506..34e1b4db83 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -700,6 +700,14 @@ func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
}
+// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs
+func UsersInTeamsCount(userIDs []int64, teamIDs []int64) (count int64, err error) {
+ if count, err = x.In("uid", userIDs).In("team_id", teamIDs).Count(new(TeamUser)); err != nil {
+ return 0, err
+ }
+ return
+}
+
// ___________ __________
// \__ ___/___ _____ _____\______ \ ____ ______ ____
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \
diff --git a/models/org_team_test.go b/models/org_team_test.go
index f9f1f289ec..87bfbb4841 100644
--- a/models/org_team_test.go
+++ b/models/org_team_test.go
@@ -346,3 +346,17 @@ func TestHasTeamRepo(t *testing.T) {
test(2, 3, true)
test(2, 5, false)
}
+
+func TestUsersInTeamsCount(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ test := func(teamIDs []int64, userIDs []int64, expected int64) {
+ count, err := UsersInTeamsCount(teamIDs, userIDs)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, count)
+ }
+
+ test([]int64{2}, []int64{1, 2, 3, 4}, 2)
+ test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2)
+ test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3)
+}
diff --git a/models/pull.go b/models/pull.go
index e97faa8f51..0041f83dc8 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -60,14 +60,15 @@ type PullRequest struct {
Issue *Issue `xorm:"-"`
Index int64
- HeadRepoID int64 `xorm:"INDEX"`
- HeadRepo *Repository `xorm:"-"`
- BaseRepoID int64 `xorm:"INDEX"`
- BaseRepo *Repository `xorm:"-"`
- HeadUserName string
- HeadBranch string
- BaseBranch string
- MergeBase string `xorm:"VARCHAR(40)"`
+ HeadRepoID int64 `xorm:"INDEX"`
+ HeadRepo *Repository `xorm:"-"`
+ BaseRepoID int64 `xorm:"INDEX"`
+ BaseRepo *Repository `xorm:"-"`
+ HeadUserName string
+ HeadBranch string
+ BaseBranch string
+ ProtectedBranch *ProtectedBranch `xorm:"-"`
+ MergeBase string `xorm:"VARCHAR(40)"`
HasMerged bool `xorm:"INDEX"`
MergedCommitID string `xorm:"VARCHAR(40)"`
@@ -110,6 +111,12 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) {
return err
}
+// LoadProtectedBranch loads the protected branch of the base branch
+func (pr *PullRequest) LoadProtectedBranch() (err error) {
+ pr.ProtectedBranch, err = GetProtectedBranchBy(pr.BaseRepo.ID, pr.BaseBranch)
+ return
+}
+
// GetDefaultMergeMessage returns default message used when merging pull request
func (pr *PullRequest) GetDefaultMergeMessage() string {
if pr.HeadRepo == nil {
@@ -288,7 +295,7 @@ func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
}
}
- if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr.BaseBranch, doer); err != nil {
+ if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr, pr.BaseBranch, doer); err != nil {
return fmt.Errorf("IsProtectedBranch: %v", err)
} else if protected {
return ErrNotAllowedToMerge{
diff --git a/models/review.go b/models/review.go
index 3ae1dd457c..91b6d6dbb2 100644
--- a/models/review.go
+++ b/models/review.go
@@ -161,6 +161,23 @@ func GetReviewByID(id int64) (*Review, error) {
return getReviewByID(x, id)
}
+func getUniqueApprovalsByPullRequestID(e Engine, prID int64) (reviews []*Review, err error) {
+ reviews = make([]*Review, 0)
+ if err := e.
+ Where("issue_id = ? AND type = ?", prID, ReviewTypeApprove).
+ OrderBy("updated_unix").
+ GroupBy("reviewer_id").
+ Find(&reviews); err != nil {
+ return nil, err
+ }
+ return
+}
+
+// GetUniqueApprovalsByPullRequestID returns all reviews submitted for a specific pull request
+func GetUniqueApprovalsByPullRequestID(prID int64) ([]*Review, error) {
+ return getUniqueApprovalsByPullRequestID(x, prID)
+}
+
// FindReviewOptions represent possible filters to find reviews
type FindReviewOptions struct {
Type ReviewType