Skip to content

Commit bac4b78

Browse files
davidsvantessontechknowlogick
authored andcommitted
Branch protection: Possibility to not use whitelist but allow anyone with write access (#9055)
* Possibility to not use whitelist but allow anyone with write access * fix existing test * rename migration function * Try to give a better name for migration step * Clear settings if higher level setting is not set * Move official reviews to db instead of counting approvals each time * migration * fix * fix migration * fix migration * Remove NOT NULL from EnableWhitelist as migration isn't possible * Fix migration, reviews are connected to issues. * Fix SQL query issues in GetReviewersByPullID. * Simplify function GetReviewersByIssueID * Handle reviewers that has been deleted * Ensure reviews for test is in a well defined order * Only clear and set official reviews when it is an approve or reject.
1 parent 6460284 commit bac4b78

File tree

20 files changed

+389
-171
lines changed

20 files changed

+389
-171
lines changed

integrations/git_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ func doProtectBranch(ctx APITestContext, branch string, userToWhitelist string)
383383
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
384384
"_csrf": csrf,
385385
"protected": "on",
386+
"enable_push": "whitelist",
386387
"enable_whitelist": "on",
387388
"whitelist_users": strconv.FormatInt(user.ID, 10),
388389
})

models/branches.go

+60-22
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type ProtectedBranch struct {
3939
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
4040
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
4141
StatusCheckContexts []string `xorm:"JSON TEXT"`
42+
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
4243
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
4344
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
4445
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
@@ -53,10 +54,25 @@ func (protectBranch *ProtectedBranch) IsProtected() bool {
5354

5455
// CanUserPush returns if some user could push to this protected branch
5556
func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
56-
if !protectBranch.EnableWhitelist {
57+
if !protectBranch.CanPush {
5758
return false
5859
}
5960

61+
if !protectBranch.EnableWhitelist {
62+
if user, err := GetUserByID(userID); err != nil {
63+
log.Error("GetUserByID: %v", err)
64+
return false
65+
} else if repo, err := GetRepositoryByID(protectBranch.RepoID); err != nil {
66+
log.Error("GetRepositoryByID: %v", err)
67+
return false
68+
} else if writeAccess, err := HasAccessUnit(user, repo, UnitTypeCode, AccessModeWrite); err != nil {
69+
log.Error("HasAccessUnit: %v", err)
70+
return false
71+
} else {
72+
return writeAccess
73+
}
74+
}
75+
6076
if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
6177
return true
6278
}
@@ -95,6 +111,38 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
95111
return in
96112
}
97113

114+
// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
115+
func (protectBranch *ProtectedBranch) IsUserOfficialReviewer(user *User) (bool, error) {
116+
return protectBranch.isUserOfficialReviewer(x, user)
117+
}
118+
119+
func (protectBranch *ProtectedBranch) isUserOfficialReviewer(e Engine, user *User) (bool, error) {
120+
repo, err := getRepositoryByID(e, protectBranch.RepoID)
121+
if err != nil {
122+
return false, err
123+
}
124+
125+
if !protectBranch.EnableApprovalsWhitelist {
126+
// Anyone with write access is considered official reviewer
127+
writeAccess, err := hasAccessUnit(e, user, repo, UnitTypeCode, AccessModeWrite)
128+
if err != nil {
129+
return false, err
130+
}
131+
return writeAccess, nil
132+
}
133+
134+
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) {
135+
return true, nil
136+
}
137+
138+
inTeam, err := isUserInTeams(e, user.ID, protectBranch.ApprovalsWhitelistTeamIDs)
139+
if err != nil {
140+
return false, err
141+
}
142+
143+
return inTeam, nil
144+
}
145+
98146
// HasEnoughApprovals returns true if pr has enough granted approvals.
99147
func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
100148
if protectBranch.RequiredApprovals == 0 {
@@ -105,30 +153,16 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
105153

106154
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
107155
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
108-
reviews, err := GetReviewersByPullID(pr.IssueID)
156+
approvals, err := x.Where("issue_id = ?", pr.Issue.ID).
157+
And("type = ?", ReviewTypeApprove).
158+
And("official = ?", true).
159+
Count(new(Review))
109160
if err != nil {
110-
log.Error("GetReviewersByPullID: %v", err)
161+
log.Error("GetGrantedApprovalsCount: %v", err)
111162
return 0
112163
}
113164

114-
approvals := int64(0)
115-
userIDs := make([]int64, 0)
116-
for _, review := range reviews {
117-
if review.Type != ReviewTypeApprove {
118-
continue
119-
}
120-
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, review.ID) {
121-
approvals++
122-
continue
123-
}
124-
userIDs = append(userIDs, review.ID)
125-
}
126-
approvalTeamCount, err := UsersInTeamsCount(userIDs, protectBranch.ApprovalsWhitelistTeamIDs)
127-
if err != nil {
128-
log.Error("UsersInTeamsCount: %v", err)
129-
return 0
130-
}
131-
return approvalTeamCount + approvals
165+
return approvals
132166
}
133167

134168
// GetProtectedBranchByRepoID getting protected branch by repo ID
@@ -139,8 +173,12 @@ func GetProtectedBranchByRepoID(repoID int64) ([]*ProtectedBranch, error) {
139173

140174
// GetProtectedBranchBy getting protected branch by ID/Name
141175
func GetProtectedBranchBy(repoID int64, branchName string) (*ProtectedBranch, error) {
176+
return getProtectedBranchBy(x, repoID, branchName)
177+
}
178+
179+
func getProtectedBranchBy(e Engine, repoID int64, branchName string) (*ProtectedBranch, error) {
142180
rel := &ProtectedBranch{RepoID: repoID, BranchName: branchName}
143-
has, err := x.Get(rel)
181+
has, err := e.Get(rel)
144182
if err != nil {
145183
return nil, err
146184
}

models/fixtures/review.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -44,38 +44,38 @@
4444
reviewer_id: 2
4545
issue_id: 3
4646
content: "New review 3"
47-
updated_unix: 946684810
48-
created_unix: 946684810
47+
updated_unix: 946684811
48+
created_unix: 946684811
4949
-
5050
id: 7
5151
type: 3
5252
reviewer_id: 3
5353
issue_id: 3
5454
content: "New review 4"
55-
updated_unix: 946684810
56-
created_unix: 946684810
55+
updated_unix: 946684812
56+
created_unix: 946684812
5757
-
5858
id: 8
5959
type: 1
6060
reviewer_id: 4
6161
issue_id: 3
6262
content: "New review 5"
63-
updated_unix: 946684810
64-
created_unix: 946684810
63+
updated_unix: 946684813
64+
created_unix: 946684813
6565
-
6666
id: 9
6767
type: 3
6868
reviewer_id: 2
6969
issue_id: 3
7070
content: "New review 3 rejected"
71-
updated_unix: 946684810
72-
created_unix: 946684810
71+
updated_unix: 946684814
72+
created_unix: 946684814
7373

7474
-
7575
id: 10
7676
type: 3
7777
reviewer_id: 100
7878
issue_id: 3
7979
content: "a deleted user's review"
80-
updated_unix: 946684810
81-
created_unix: 946684810
80+
updated_unix: 946684815
81+
created_unix: 946684815

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ var migrations = []Migration{
276276
NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
277277
// v110 -> v111
278278
NewMigration("change review content type to text", changeReviewContentToText),
279+
// v111 -> v112
280+
NewMigration("update branch protection for can push and whitelist enable", addBranchProtectionCanPushAndEnableWhitelist),
279281
}
280282

281283
// Migrate database to current version

models/migrations/v111.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2019 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+
"code.gitea.io/gitea/models"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
14+
15+
type ProtectedBranch struct {
16+
CanPush bool `xorm:"NOT NULL DEFAULT false"`
17+
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
18+
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
19+
}
20+
21+
type Review struct {
22+
ID int64 `xorm:"pk autoincr"`
23+
Official bool `xorm:"NOT NULL DEFAULT false"`
24+
}
25+
26+
sess := x.NewSession()
27+
defer sess.Close()
28+
29+
if err := sess.Sync2(new(ProtectedBranch)); err != nil {
30+
return err
31+
}
32+
33+
if err := sess.Sync2(new(Review)); err != nil {
34+
return err
35+
}
36+
37+
if _, err := sess.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil {
38+
return err
39+
}
40+
if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil {
41+
return err
42+
}
43+
44+
var pageSize int64 = 20
45+
qresult, err := sess.QueryInterface("SELECT max(id) as max_id FROM issue")
46+
if err != nil {
47+
return err
48+
}
49+
var totalIssues int64
50+
totalIssues, ok := qresult[0]["max_id"].(int64)
51+
if !ok {
52+
// If there are no issues at all we ignore it
53+
return nil
54+
}
55+
totalPages := totalIssues / pageSize
56+
57+
// Find latest review of each user in each pull request, and set official field if appropriate
58+
reviews := []*models.Review{}
59+
var page int64
60+
for page = 0; page <= totalPages; page++ {
61+
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)",
62+
page*pageSize, (page+1)*pageSize, models.ReviewTypeApprove, models.ReviewTypeReject).
63+
Find(&reviews); err != nil {
64+
return err
65+
}
66+
67+
for _, review := range reviews {
68+
if err := review.LoadAttributes(); err != nil {
69+
// Error might occur if user or issue doesn't exist, ignore it.
70+
continue
71+
}
72+
official, err := models.IsOfficialReviewer(review.Issue, review.Reviewer)
73+
if err != nil {
74+
// Branch might not be proteced or other error, ignore it.
75+
continue
76+
}
77+
review.Official = official
78+
79+
if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil {
80+
return err
81+
}
82+
}
83+
84+
}
85+
86+
return sess.Commit()
87+
}

models/org_team.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,11 @@ func RemoveTeamMember(team *Team, userID int64) error {
914914

915915
// IsUserInTeams returns if a user in some teams
916916
func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
917-
return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
917+
return isUserInTeams(x, userID, teamIDs)
918+
}
919+
920+
func isUserInTeams(e Engine, userID int64, teamIDs []int64) (bool, error) {
921+
return e.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
918922
}
919923

920924
// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs

models/pull.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,20 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) {
159159

160160
// LoadProtectedBranch loads the protected branch of the base branch
161161
func (pr *PullRequest) LoadProtectedBranch() (err error) {
162+
return pr.loadProtectedBranch(x)
163+
}
164+
165+
func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) {
162166
if pr.BaseRepo == nil {
163167
if pr.BaseRepoID == 0 {
164168
return nil
165169
}
166-
pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
170+
pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID)
167171
if err != nil {
168172
return
169173
}
170174
}
171-
pr.ProtectedBranch, err = GetProtectedBranchBy(pr.BaseRepo.ID, pr.BaseBranch)
175+
pr.ProtectedBranch, err = getProtectedBranchBy(e, pr.BaseRepo.ID, pr.BaseBranch)
172176
return
173177
}
174178

0 commit comments

Comments
 (0)