diff options
author | Jonas Franz <info@jonasfranz.software> | 2018-12-11 12:28:37 +0100 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2018-12-11 19:28:37 +0800 |
commit | 9681c8373426e8364760f033b9935b81b9276351 (patch) | |
tree | d9308c33809fdfa7859fd9a1057432a74a28c92f /models | |
parent | 64680b72bd1b0a0fc164296ff0cc7319ae7dc4ca (diff) | |
download | gitea-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.go | 104 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v74.go | 16 | ||||
-rw-r--r-- | models/org_team.go | 8 | ||||
-rw-r--r-- | models/org_team_test.go | 14 | ||||
-rw-r--r-- | models/pull.go | 25 | ||||
-rw-r--r-- | models/review.go | 17 |
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 |