forgejo/routers/private/hook.go
zeripath 0bfe5eb10b
Allow Protected Branches to Whitelist Deploy Keys ()
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  

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
2019-10-21 09:21:45 +01:00

250 lines
8.7 KiB
Go

// 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 private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
import (
"fmt"
"net/http"
"os"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/repofiles"
"code.gitea.io/gitea/modules/util"
"gitea.com/macaron/macaron"
)
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *macaron.Context) {
ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo")
oldCommitID := ctx.QueryTrim("old")
newCommitID := ctx.QueryTrim("new")
refFullName := ctx.QueryTrim("ref")
userID := ctx.QueryInt64("userID")
gitObjectDirectory := ctx.QueryTrim("gitObjectDirectory")
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)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": err.Error(),
})
return
}
repo.OwnerName = ownerName
protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
if err != nil {
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
ctx.JSON(500, map[string]interface{}{
"err": err.Error(),
})
return
}
if protectBranch != nil && protectBranch.IsProtected() {
// check and deletion
if newCommitID == git.EmptySHA {
log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("branch %s is protected from deletion", branchName),
})
return
}
// detect force push
if git.EmptySHA != oldCommitID {
env := os.Environ()
if gitAlternativeObjectDirectories != "" {
env = append(env,
private.GitAlternativeObjectDirectories+"="+gitAlternativeObjectDirectories)
}
if gitObjectDirectory != "" {
env = append(env,
private.GitObjectDirectory+"="+gitObjectDirectory)
}
if gitQuarantinePath != "" {
env = append(env,
private.GitQuarantinePath+"="+gitQuarantinePath)
}
output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env)
if err != nil {
log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Fail to detect force push: %v", err),
})
return
} else if len(output) > 0 {
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("branch %s is protected from force push", branchName),
})
return
}
}
canPush := false
if isDeployKey {
canPush = protectBranch.WhitelistDeployKeys
} else {
canPush = protectBranch.CanUserPush(userID)
}
if !canPush && prID > 0 {
pr, err := models.GetPullRequestByID(prID)
if err != nil {
log.Error("Unable to get PullRequest %d Error: %v", prID, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", prID, err),
})
return
}
if !protectBranch.HasEnoughApprovals(pr) {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", userID, branchName, repo, pr.Index)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, prID),
})
return
}
} else if !canPush {
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", userID, branchName, repo)
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"err": fmt.Sprintf("protected branch %s can not be pushed to", branchName),
})
return
}
}
ctx.PlainText(http.StatusOK, []byte("ok"))
}
// HookPostReceive updates services and users
func HookPostReceive(ctx *macaron.Context) {
ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo")
oldCommitID := ctx.Query("old")
newCommitID := ctx.Query("new")
refFullName := ctx.Query("ref")
userID := ctx.QueryInt64("userID")
userName := ctx.Query("username")
branch := refFullName
if strings.HasPrefix(refFullName, git.BranchPrefix) {
branch = strings.TrimPrefix(refFullName, git.BranchPrefix)
} else if strings.HasPrefix(refFullName, git.TagPrefix) {
branch = strings.TrimPrefix(refFullName, git.TagPrefix)
}
// Only trigger activity updates for changes to branches or
// tags. Updates to other refs (eg, refs/notes, refs/changes,
// or other less-standard refs spaces are ignored since there
// may be a very large number of them).
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if err := repofiles.PushUpdate(repo, branch, models.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: oldCommitID,
NewCommitID: newCommitID,
PusherID: userID,
PusherName: userName,
RepoUserName: ownerName,
RepoName: repoName,
}); err != nil {
log.Error("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err),
})
return
}
}
if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
repo.OwnerName = ownerName
pullRequestAllowed := repo.AllowsPulls()
if !pullRequestAllowed {
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": false,
})
return
}
baseRepo := repo
if repo.IsFork {
if err := repo.GetBaseRepo(); err != nil {
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
})
return
}
baseRepo = repo.BaseRepo
}
if !repo.IsFork && branch == baseRepo.DefaultBranch {
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": false,
})
return
}
pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch)
if err != nil && !models.IsErrPullRequestNotExist(err) {
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": fmt.Sprintf(
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
})
return
}
if pr == nil {
if repo.IsFork {
branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": true,
"create": true,
"branch": branch,
"url": fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)),
})
} else {
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": true,
"create": false,
"branch": branch,
"url": fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index),
})
}
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"message": false,
})
}