From 0bfe5eb10b1953cb1f85f7a7b6eb5f24724b8021 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Mon, 21 Oct 2019 09:21:45 +0100
Subject: [PATCH] Allow Protected Branches to Whitelist Deploy Keys (#8483)

Add an option to protected branches to add writing deploy keys to the whitelist for pushing.

Please note this is technically a breaking change: previously if the owner of a repository was on the whitelist then any writing deploy key was effectively on the whitelist. This option will now need to be set if that is desired.

Closes #8472

Details:
* Allow Protected Branches to Whitelist Deploy Keys
* Add migration
* Ensure that IsDeployKey is set to false on the http pushes
* add not null default false
---
 cmd/hook.go                                   |  2 ++
 cmd/serv.go                                   |  2 ++
 models/branches.go                            |  1 +
 models/migrations/migrations.go               |  2 ++
 models/migrations/v103.go                     | 18 ++++++++++++++++++
 models/update.go                              |  2 ++
 modules/auth/repo_form.go                     |  1 +
 modules/private/hook.go                       |  4 +++-
 options/locale/locale_en-US.ini               |  1 +
 routers/private/hook.go                       |  8 +++++++-
 routers/repo/http.go                          |  1 +
 routers/repo/setting_protected_branch.go      |  1 +
 templates/repo/settings/protected_branch.tmpl |  7 +++++++
 13 files changed, 48 insertions(+), 2 deletions(-)
 create mode 100644 models/migrations/v103.go

diff --git a/cmd/hook.go b/cmd/hook.go
index f5b7962aab..f07568dd8b 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -66,6 +66,7 @@ func runHookPreReceive(c *cli.Context) error {
 	reponame := os.Getenv(models.EnvRepoName)
 	userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
 	prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64)
+	isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey))
 
 	buf := bytes.NewBuffer(nil)
 	scanner := bufio.NewScanner(os.Stdin)
@@ -98,6 +99,7 @@ func runHookPreReceive(c *cli.Context) error {
 				GitObjectDirectory:              os.Getenv(private.GitObjectDirectory),
 				GitQuarantinePath:               os.Getenv(private.GitQuarantinePath),
 				ProtectedBranchID:               prID,
+				IsDeployKey:                     isDeployKey,
 			})
 			switch statusCode {
 			case http.StatusInternalServerError:
diff --git a/cmd/serv.go b/cmd/serv.go
index 6533b0371c..1ac6b21e53 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -191,6 +191,8 @@ func runServ(c *cli.Context) error {
 	os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10))
 	os.Setenv(models.ProtectedBranchRepoID, strconv.FormatInt(results.RepoID, 10))
 	os.Setenv(models.ProtectedBranchPRID, fmt.Sprintf("%d", 0))
+	os.Setenv(models.EnvIsDeployKey, fmt.Sprintf("%t", results.IsDeployKey))
+	os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID))
 
 	//LFS token authentication
 	if verb == lfsAuthenticateVerb {
diff --git a/models/branches.go b/models/branches.go
index fa8beb866c..c5f227f1e5 100644
--- a/models/branches.go
+++ b/models/branches.go
@@ -34,6 +34,7 @@ type ProtectedBranch struct {
 	WhitelistUserIDs          []int64            `xorm:"JSON TEXT"`
 	WhitelistTeamIDs          []int64            `xorm:"JSON TEXT"`
 	EnableMergeWhitelist      bool               `xorm:"NOT NULL DEFAULT false"`
+	WhitelistDeployKeys       bool               `xorm:"NOT NULL DEFAULT false"`
 	MergeWhitelistUserIDs     []int64            `xorm:"JSON TEXT"`
 	MergeWhitelistTeamIDs     []int64            `xorm:"JSON TEXT"`
 	EnableStatusCheck         bool               `xorm:"NOT NULL DEFAULT false"`
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 8064eccfc1..8b1329ea68 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -260,6 +260,8 @@ var migrations = []Migration{
 	NewMigration("change length of some external login users columns", changeSomeColumnsLengthOfExternalLoginUser),
 	// v102 -> v103
 	NewMigration("update migration repositories' service type", dropColumnHeadUserNameOnPullRequest),
+	// v103 -> v104
+	NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v103.go b/models/migrations/v103.go
new file mode 100644
index 0000000000..fed025c5cd
--- /dev/null
+++ b/models/migrations/v103.go
@@ -0,0 +1,18 @@
+// Copyright 2019 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 (
+	"xorm.io/xorm"
+)
+
+func addWhitelistDeployKeysToBranches(x *xorm.Engine) error {
+	type ProtectedBranch struct {
+		ID                  int64
+		WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
+	}
+
+	return x.Sync2(new(ProtectedBranch))
+}
diff --git a/models/update.go b/models/update.go
index c6ea1a845e..5e941c22c4 100644
--- a/models/update.go
+++ b/models/update.go
@@ -22,6 +22,8 @@ const (
 	EnvPusherName   = "GITEA_PUSHER_NAME"
 	EnvPusherEmail  = "GITEA_PUSHER_EMAIL"
 	EnvPusherID     = "GITEA_PUSHER_ID"
+	EnvKeyID        = "GITEA_KEY_ID"
+	EnvIsDeployKey  = "GITEA_IS_DEPLOY_KEY"
 )
 
 // CommitToPushCommit transforms a git.Commit to PushCommit type.
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 5a8ac5934f..2280666114 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -152,6 +152,7 @@ type ProtectBranchForm struct {
 	EnableWhitelist         bool
 	WhitelistUsers          string
 	WhitelistTeams          string
+	WhitelistDeployKeys     bool
 	EnableMergeWhitelist    bool
 	MergeWhitelistUsers     string
 	MergeWhitelistTeams     string
diff --git a/modules/private/hook.go b/modules/private/hook.go
index 67496b5132..cc9703cc77 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -31,11 +31,12 @@ type HookOptions struct {
 	GitAlternativeObjectDirectories string
 	GitQuarantinePath               string
 	ProtectedBranchID               int64
+	IsDeployKey                     bool
 }
 
 // HookPreReceive check whether the provided commits are allowed
 func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) {
-	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&gitQuarantinePath=%s&prID=%d",
+	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&gitQuarantinePath=%s&prID=%d&isDeployKey=%t",
 		url.PathEscape(ownerName),
 		url.PathEscape(repoName),
 		url.QueryEscape(opts.OldCommitID),
@@ -46,6 +47,7 @@ func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string)
 		url.QueryEscape(opts.GitAlternativeObjectDirectories),
 		url.QueryEscape(opts.GitQuarantinePath),
 		opts.ProtectedBranchID,
+		opts.IsDeployKey,
 	)
 
 	resp, err := newInternalRequest(reqURL, "GET").Response()
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 7b65de6add..eb38a777c8 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1334,6 +1334,7 @@ settings.protect_this_branch = Enable Branch Protection
 settings.protect_this_branch_desc = Prevent deletion and disable any Git pushing to the branch.
 settings.protect_whitelist_committers = Enable Push Whitelist
 settings.protect_whitelist_committers_desc = Allow whitelisted users or teams to push to this branch (but not force push).
+settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push
 settings.protect_whitelist_users = Whitelisted users for pushing:
 settings.protect_whitelist_search_users = Search users…
 settings.protect_whitelist_teams = Whitelisted teams for pushing:
diff --git a/routers/private/hook.go b/routers/private/hook.go
index 1f6ab2f673..074e3aef19 100644
--- a/routers/private/hook.go
+++ b/routers/private/hook.go
@@ -33,6 +33,7 @@ func HookPreReceive(ctx *macaron.Context) {
 	gitAlternativeObjectDirectories := ctx.QueryTrim("gitAlternativeObjectDirectories")
 	gitQuarantinePath := ctx.QueryTrim("gitQuarantinePath")
 	prID := ctx.QueryInt64("prID")
+	isDeployKey := ctx.QueryBool("isDeployKey")
 
 	branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
 	repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
@@ -95,7 +96,12 @@ func HookPreReceive(ctx *macaron.Context) {
 			}
 		}
 
-		canPush := protectBranch.CanUserPush(userID)
+		canPush := false
+		if isDeployKey {
+			canPush = protectBranch.WhitelistDeployKeys
+		} else {
+			canPush = protectBranch.CanUserPush(userID)
+		}
 		if !canPush && prID > 0 {
 			pr, err := models.GetPullRequestByID(prID)
 			if err != nil {
diff --git a/routers/repo/http.go b/routers/repo/http.go
index 09dd820585..d41c63ba35 100644
--- a/routers/repo/http.go
+++ b/routers/repo/http.go
@@ -263,6 +263,7 @@ func HTTP(ctx *context.Context) {
 			models.EnvPusherName + "=" + authUser.Name,
 			models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
 			models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID),
+			models.EnvIsDeployKey + "=false",
 		}
 
 		if !authUser.KeepEmailPrivate {
diff --git a/routers/repo/setting_protected_branch.go b/routers/repo/setting_protected_branch.go
index 2a8502e6f4..bc4d7c3a9e 100644
--- a/routers/repo/setting_protected_branch.go
+++ b/routers/repo/setting_protected_branch.go
@@ -213,6 +213,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
 
 		protectBranch.EnableStatusCheck = f.EnableStatusCheck
 		protectBranch.StatusCheckContexts = f.StatusCheckContexts
+		protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
 
 		protectBranch.RequiredApprovals = f.RequiredApprovals
 		if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
index 067d1d9761..a50765c4b4 100644
--- a/templates/repo/settings/protected_branch.tmpl
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -59,6 +59,13 @@
 								</div>
 							</div>
 						{{end}}
+						<br>
+						<div class="whitelist field">
+							<div class="ui checkbox">
+								<input type="checkbox" name="whitelist_deploy_keys" {{if .Branch.WhitelistDeployKeys}}checked{{end}}>
+								<label for="whitelist_deploy_keys">{{.i18n.Tr "repo.settings.protect_whitelist_deploy_keys"}}</label>
+							</div>
+						</div>
 					</div>
 
 					<div class="field">