2021-09-16 09:34:54 -04:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2021-09-16 09:34:54 -04:00
// 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"
2022-03-22 05:29:07 -04:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
2022-06-12 11:51:54 -04:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 05:37:59 -04:00
issues_model "code.gitea.io/gitea/models/issues"
2022-03-22 05:29:07 -04:00
perm_model "code.gitea.io/gitea/models/perm"
2022-05-11 06:09:36 -04:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-11-09 14:57:58 -05:00
"code.gitea.io/gitea/models/unit"
2021-11-24 04:49:20 -05:00
user_model "code.gitea.io/gitea/models/user"
2021-09-16 09:34:54 -04:00
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/web"
pull_service "code.gitea.io/gitea/services/pull"
)
type preReceiveContext struct {
* gitea_context . PrivateContext
2022-03-22 05:29:07 -04:00
// loadedPusher indicates that where the following information are loaded
loadedPusher bool
user * user_model . User // it's the org user if a DeployKey is used
2022-05-11 06:09:36 -04:00
userPerm access_model . Permission
2022-03-22 05:29:07 -04:00
deployKeyAccessMode perm_model . AccessMode
2021-09-16 09:34:54 -04:00
canCreatePullRequest bool
checkedCanCreatePullRequest bool
canWriteCode bool
checkedCanWriteCode bool
2022-06-12 11:51:54 -04:00
protectedTags [ ] * git_model . ProtectedTag
2021-09-16 09:34:54 -04:00
gotProtectedTags bool
env [ ] string
opts * private . HookOptions
2022-04-28 11:45:33 -04:00
branchName string
2021-09-16 09:34:54 -04:00
}
2022-03-22 05:29:07 -04:00
// CanWriteCode returns true if pusher can write code
2021-09-16 09:34:54 -04:00
func ( ctx * preReceiveContext ) CanWriteCode ( ) bool {
if ! ctx . checkedCanWriteCode {
2022-03-22 05:29:07 -04:00
if ! ctx . loadPusherAndPermission ( ) {
return false
}
2022-06-13 05:37:59 -04:00
ctx . canWriteCode = issues_model . CanMaintainerWriteToBranch ( ctx . userPerm , ctx . branchName , ctx . user ) || ctx . deployKeyAccessMode >= perm_model . AccessModeWrite
2021-09-16 09:34:54 -04:00
ctx . checkedCanWriteCode = true
}
return ctx . canWriteCode
}
2022-03-22 05:29:07 -04:00
// AssertCanWriteCode returns true if pusher can write code
2021-09-16 09:34:54 -04:00
func ( ctx * preReceiveContext ) AssertCanWriteCode ( ) bool {
if ! ctx . CanWriteCode ( ) {
if ctx . Written ( ) {
return false
}
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2022-03-22 05:29:07 -04:00
"err" : "User permission denied for writing." ,
2021-09-16 09:34:54 -04:00
} )
return false
}
return true
}
2022-03-22 05:29:07 -04:00
// CanCreatePullRequest returns true if pusher can create pull requests
2021-09-16 09:34:54 -04:00
func ( ctx * preReceiveContext ) CanCreatePullRequest ( ) bool {
if ! ctx . checkedCanCreatePullRequest {
2022-03-22 05:29:07 -04:00
if ! ctx . loadPusherAndPermission ( ) {
return false
}
ctx . canCreatePullRequest = ctx . userPerm . CanRead ( unit . TypePullRequests )
2021-09-16 09:34:54 -04:00
ctx . checkedCanCreatePullRequest = true
}
return ctx . canCreatePullRequest
}
2022-03-22 05:29:07 -04:00
// AssertCreatePullRequest returns true if can create pull requests
2021-09-16 09:34:54 -04:00
func ( ctx * preReceiveContext ) AssertCreatePullRequest ( ) bool {
if ! ctx . CanCreatePullRequest ( ) {
if ctx . Written ( ) {
return false
}
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2022-03-22 05:29:07 -04:00
"err" : "User permission denied for creating pull-request." ,
2021-09-16 09:34:54 -04:00
} )
return false
}
return true
}
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive ( ctx * gitea_context . PrivateContext ) {
opts := web . GetForm ( ctx ) . ( * private . HookOptions )
ourCtx := & preReceiveContext {
PrivateContext : ctx ,
env : generateGitEnv ( opts ) , // Generate git environment for checking commits
opts : opts ,
}
// Iterate across the provided old commit IDs
for i := range opts . OldCommitIDs {
oldCommitID := opts . OldCommitIDs [ i ]
newCommitID := opts . NewCommitIDs [ i ]
refFullName := opts . RefFullNames [ i ]
switch {
case strings . HasPrefix ( refFullName , git . BranchPrefix ) :
preReceiveBranch ( ourCtx , oldCommitID , newCommitID , refFullName )
case strings . HasPrefix ( refFullName , git . TagPrefix ) :
preReceiveTag ( ourCtx , oldCommitID , newCommitID , refFullName )
case git . SupportProcReceive && strings . HasPrefix ( refFullName , git . PullRequestPrefix ) :
preReceivePullRequest ( ourCtx , oldCommitID , newCommitID , refFullName )
default :
ourCtx . AssertCanWriteCode ( )
}
if ctx . Written ( ) {
return
}
}
2021-12-15 01:59:57 -05:00
ctx . PlainText ( http . StatusOK , "ok" )
2021-09-16 09:34:54 -04:00
}
func preReceiveBranch ( ctx * preReceiveContext , oldCommitID , newCommitID , refFullName string ) {
2022-04-28 11:45:33 -04:00
branchName := strings . TrimPrefix ( refFullName , git . BranchPrefix )
ctx . branchName = branchName
2021-09-16 09:34:54 -04:00
if ! ctx . AssertCanWriteCode ( ) {
return
}
repo := ctx . Repo . Repository
gitRepo := ctx . Repo . GitRepo
if branchName == repo . DefaultBranch && newCommitID == git . EmptySHA {
log . Warn ( "Forbidden: Branch: %s is the default branch in %-v and cannot be deleted" , branchName , repo )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "branch %s is the default branch and cannot be deleted" , branchName ) ,
} )
return
}
2022-06-12 11:51:54 -04:00
protectBranch , err := git_model . GetProtectedBranchBy ( ctx , repo . ID , branchName )
2021-09-16 09:34:54 -04:00
if err != nil {
log . Error ( "Unable to get protected branch: %s in %-v Error: %v" , branchName , repo , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : err . Error ( ) ,
} )
return
}
// Allow pushes to non-protected branches
if protectBranch == nil || ! protectBranch . IsProtected ( ) {
return
}
// This ref is a protected branch.
//
// First of all we need to enforce absolutely:
//
// 1. Detect and prevent deletion of the branch
if newCommitID == git . EmptySHA {
log . Warn ( "Forbidden: Branch: %s in %-v is protected from deletion" , branchName , repo )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "branch %s is protected from deletion" , branchName ) ,
} )
return
}
// 2. Disallow force pushes to protected branches
if git . EmptySHA != oldCommitID {
2022-10-23 10:44:45 -04:00
output , _ , err := git . NewCommand ( ctx , "rev-list" , "--max-count=1" ) . AddDynamicArguments ( oldCommitID , "^" + newCommitID ) . RunStdString ( & git . RunOpts { Dir : repo . RepoPath ( ) , Env : ctx . env } )
2021-09-16 09:34:54 -04:00
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 , private . Response {
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 , private . Response {
Err : fmt . Sprintf ( "branch %s is protected from force push" , branchName ) ,
} )
return
}
}
// 3. Enforce require signed commits
if protectBranch . RequireSignedCommits {
err := verifyCommits ( oldCommitID , newCommitID , gitRepo , ctx . env )
if err != nil {
if ! isErrUnverifiedCommit ( err ) {
log . Error ( "Unable to check commits from %s to %s in %-v: %v" , oldCommitID , newCommitID , repo , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to check commits from %s to %s: %v" , oldCommitID , newCommitID , err ) ,
} )
return
}
unverifiedCommit := err . ( * errUnverifiedCommit ) . sha
log . Warn ( "Forbidden: Branch: %s in %-v is protected from unverified commit %s" , branchName , repo , unverifiedCommit )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "branch %s is protected from unverified commit %s" , branchName , unverifiedCommit ) ,
} )
return
}
}
// Now there are several tests which can be overridden:
//
// 4. Check protected file patterns - this is overridable from the UI
changedProtectedfiles := false
protectedFilePath := ""
globs := protectBranch . GetProtectedFilePatterns ( )
if len ( globs ) > 0 {
2022-01-19 18:26:57 -05:00
_ , err := pull_service . CheckFileProtection ( gitRepo , oldCommitID , newCommitID , globs , 1 , ctx . env )
2021-09-16 09:34:54 -04:00
if err != nil {
if ! models . IsErrFilePathProtected ( err ) {
log . Error ( "Unable to check file protection for commits from %s to %s in %-v: %v" , oldCommitID , newCommitID , repo , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to check file protection for commits from %s to %s: %v" , oldCommitID , newCommitID , err ) ,
} )
return
}
changedProtectedfiles = true
protectedFilePath = err . ( models . ErrFilePathProtected ) . Path
}
}
// 5. Check if the doer is allowed to push
2022-06-20 06:02:49 -04:00
var canPush bool
2022-03-22 05:29:07 -04:00
if ctx . opts . DeployKeyID != 0 {
2021-09-16 09:34:54 -04:00
canPush = ! changedProtectedfiles && protectBranch . CanPush && ( ! protectBranch . EnableWhitelist || protectBranch . WhitelistDeployKeys )
} else {
2023-01-08 22:50:54 -05:00
canPush = ! changedProtectedfiles && protectBranch . CanUserPush ( ctx , ctx . opts . UserID )
2021-09-16 09:34:54 -04:00
}
// 6. If we're not allowed to push directly
if ! canPush {
// Is this is a merge from the UI/API?
if ctx . opts . PullRequestID == 0 {
// 6a. If we're not merging from the UI/API then there are two ways we got here:
//
// We are changing a protected file and we're not allowed to do that
if changedProtectedfiles {
log . Warn ( "Forbidden: Branch: %s in %-v is protected from changing file %s" , branchName , repo , protectedFilePath )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "branch %s is protected from changing file %s" , branchName , protectedFilePath ) ,
} )
return
}
// Allow commits that only touch unprotected files
globs := protectBranch . GetUnprotectedFilePatterns ( )
if len ( globs ) > 0 {
2022-01-19 18:26:57 -05:00
unprotectedFilesOnly , err := pull_service . CheckUnprotectedFiles ( gitRepo , oldCommitID , newCommitID , globs , ctx . env )
2021-09-16 09:34:54 -04:00
if err != nil {
log . Error ( "Unable to check file protection for commits from %s to %s in %-v: %v" , oldCommitID , newCommitID , repo , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to check file protection for commits from %s to %s: %v" , oldCommitID , newCommitID , err ) ,
} )
return
}
if unprotectedFilesOnly {
// Commit only touches unprotected files, this is allowed
return
}
}
// Or we're simply not able to push to this protected branch
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v" , ctx . opts . UserID , branchName , repo )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
} )
return
}
// 6b. Merge (from UI or API)
// Get the PR, user and permissions for the user in the repository
2022-06-13 05:37:59 -04:00
pr , err := issues_model . GetPullRequestByID ( ctx , ctx . opts . PullRequestID )
2021-09-16 09:34:54 -04:00
if err != nil {
log . Error ( "Unable to get PullRequest %d Error: %v" , ctx . opts . PullRequestID , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to get PullRequest %d Error: %v" , ctx . opts . PullRequestID , err ) ,
} )
return
}
2022-03-22 05:29:07 -04:00
// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
if ! ctx . loadPusherAndPermission ( ) {
// if error occurs, loadPusherAndPermission had written the error response
return
}
2021-09-16 09:34:54 -04:00
// Now check if the user is allowed to merge PRs for this repository
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
2022-05-03 15:46:28 -04:00
allowedMerge , err := pull_service . IsUserAllowedToMerge ( ctx , pr , ctx . userPerm , ctx . user )
2021-09-16 09:34:54 -04:00
if err != nil {
log . Error ( "Error calculating if allowed to merge: %v" , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Error calculating if allowed to merge: %v" , err ) ,
} )
return
}
if ! allowedMerge {
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d" , ctx . opts . UserID , branchName , repo , pr . Index )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
} )
return
}
// If we're an admin for the repository we can ignore status checks, reviews and override protected files
2022-03-22 05:29:07 -04:00
if ctx . userPerm . IsAdmin ( ) {
2021-09-16 09:34:54 -04:00
return
}
// Now if we're not an admin - we can't overwrite protected files so fail now
if changedProtectedfiles {
log . Warn ( "Forbidden: Branch: %s in %-v is protected from changing file %s" , branchName , repo , protectedFilePath )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "branch %s is protected from changing file %s" , branchName , protectedFilePath ) ,
} )
return
}
// Check all status checks and reviews are ok
2022-05-01 19:54:44 -04:00
if err := pull_service . CheckPullBranchProtections ( ctx , pr , true ) ; err != nil {
2022-03-31 10:53:08 -04:00
if models . IsErrDisallowedToMerge ( err ) {
2021-09-16 09:34:54 -04:00
log . Warn ( "Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s" , ctx . opts . UserID , branchName , repo , pr . Index , err . Error ( ) )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s" , branchName , ctx . opts . PullRequestID , err . Error ( ) ) ,
} )
return
}
log . Error ( "Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v" , ctx . opts . UserID , branchName , repo , pr . Index , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to get status of pull request %d. Error: %v" , ctx . opts . PullRequestID , err ) ,
} )
return
}
}
}
func preReceiveTag ( ctx * preReceiveContext , oldCommitID , newCommitID , refFullName string ) {
if ! ctx . AssertCanWriteCode ( ) {
return
}
tagName := strings . TrimPrefix ( refFullName , git . TagPrefix )
if ! ctx . gotProtectedTags {
var err error
2022-11-19 03:12:33 -05:00
ctx . protectedTags , err = git_model . GetProtectedTags ( ctx , ctx . Repo . Repository . ID )
2021-09-16 09:34:54 -04:00
if err != nil {
log . Error ( "Unable to get protected tags for %-v Error: %v" , ctx . Repo . Repository , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : err . Error ( ) ,
} )
return
}
ctx . gotProtectedTags = true
}
2022-11-19 03:12:33 -05:00
isAllowed , err := git_model . IsUserAllowedToControlTag ( ctx , ctx . protectedTags , tagName , ctx . opts . UserID )
2021-09-16 09:34:54 -04:00
if err != nil {
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : err . Error ( ) ,
} )
return
}
if ! isAllowed {
log . Warn ( "Forbidden: Tag %s in %-v is protected" , tagName , ctx . Repo . Repository )
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "Tag %s is protected" , tagName ) ,
} )
return
}
}
func preReceivePullRequest ( ctx * preReceiveContext , oldCommitID , newCommitID , refFullName string ) {
if ! ctx . AssertCreatePullRequest ( ) {
return
}
if ctx . Repo . Repository . IsEmpty {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
"err" : "Can't create pull request for an empty repository." ,
} )
return
}
if ctx . opts . IsWiki {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
2022-01-10 04:32:37 -05:00
"err" : "Pull requests are not supported on the wiki." ,
2021-09-16 09:34:54 -04:00
} )
return
}
baseBranchName := refFullName [ len ( git . PullRequestPrefix ) : ]
baseBranchExist := false
if ctx . Repo . GitRepo . IsBranchExist ( baseBranchName ) {
baseBranchExist = true
}
if ! baseBranchExist {
for p , v := range baseBranchName {
if v == '/' && ctx . Repo . GitRepo . IsBranchExist ( baseBranchName [ : p ] ) && p != len ( baseBranchName ) - 1 {
baseBranchExist = true
break
}
}
}
if ! baseBranchExist {
ctx . JSON ( http . StatusForbidden , private . Response {
Err : fmt . Sprintf ( "Unexpected ref: %s" , refFullName ) ,
} )
return
}
}
func generateGitEnv ( opts * private . HookOptions ) ( env [ ] string ) {
env = os . Environ ( )
if opts . GitAlternativeObjectDirectories != "" {
env = append ( env ,
private . GitAlternativeObjectDirectories + "=" + opts . GitAlternativeObjectDirectories )
}
if opts . GitObjectDirectory != "" {
env = append ( env ,
private . GitObjectDirectory + "=" + opts . GitObjectDirectory )
}
if opts . GitQuarantinePath != "" {
env = append ( env ,
private . GitQuarantinePath + "=" + opts . GitQuarantinePath )
}
return env
}
2022-03-22 05:29:07 -04:00
// loadPusherAndPermission returns false if an error occurs, and it writes the error response
func ( ctx * preReceiveContext ) loadPusherAndPermission ( ) bool {
if ctx . loadedPusher {
return true
}
2022-12-02 21:48:26 -05:00
user , err := user_model . GetUserByID ( ctx , ctx . opts . UserID )
2021-09-16 09:34:54 -04:00
if err != nil {
2022-03-22 05:29:07 -04:00
log . Error ( "Unable to get User id %d Error: %v" , ctx . opts . UserID , err )
2021-09-16 09:34:54 -04:00
ctx . JSON ( http . StatusInternalServerError , private . Response {
2022-03-22 05:29:07 -04:00
Err : fmt . Sprintf ( "Unable to get User id %d Error: %v" , ctx . opts . UserID , err ) ,
2021-09-16 09:34:54 -04:00
} )
2022-03-22 05:29:07 -04:00
return false
2021-09-16 09:34:54 -04:00
}
2022-03-22 05:29:07 -04:00
ctx . user = user
2021-09-16 09:34:54 -04:00
2022-05-11 06:09:36 -04:00
userPerm , err := access_model . GetUserRepoPermission ( ctx , ctx . Repo . Repository , user )
2021-09-16 09:34:54 -04:00
if err != nil {
2022-06-27 09:52:22 -04:00
log . Error ( "Unable to get Repo permission of repo %s/%s of User %s: %v" , ctx . Repo . Repository . OwnerName , ctx . Repo . Repository . Name , user . Name , err )
2021-09-16 09:34:54 -04:00
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to get Repo permission of repo %s/%s of User %s: %v" , ctx . Repo . Repository . OwnerName , ctx . Repo . Repository . Name , user . Name , err ) ,
} )
2022-03-22 05:29:07 -04:00
return false
}
ctx . userPerm = userPerm
if ctx . opts . DeployKeyID != 0 {
deployKey , err := asymkey_model . GetDeployKeyByID ( ctx , ctx . opts . DeployKeyID )
if err != nil {
log . Error ( "Unable to get DeployKey id %d Error: %v" , ctx . opts . DeployKeyID , err )
ctx . JSON ( http . StatusInternalServerError , private . Response {
Err : fmt . Sprintf ( "Unable to get DeployKey id %d Error: %v" , ctx . opts . DeployKeyID , err ) ,
} )
return false
}
ctx . deployKeyAccessMode = deployKey . Mode
2021-09-16 09:34:54 -04:00
}
2022-03-22 05:29:07 -04:00
ctx . loadedPusher = true
return true
2021-09-16 09:34:54 -04:00
}