@ -25,7 +25,6 @@ import (
"gitea.com/macaron/macaron"
"gitea.com/macaron/macaron"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing"
"github.com/gobwas/glob"
)
)
func verifyCommits ( oldCommitID , newCommitID string , repo * git . Repository , env [ ] string ) error {
func verifyCommits ( oldCommitID , newCommitID string , repo * git . Repository , env [ ] string ) error {
@ -59,53 +58,6 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
return err
return err
}
}
func checkFileProtection ( oldCommitID , newCommitID string , patterns [ ] glob . Glob , repo * git . Repository , env [ ] string ) error {
stdoutReader , stdoutWriter , err := os . Pipe ( )
if err != nil {
log . Error ( "Unable to create os.Pipe for %s" , repo . Path )
return err
}
defer func ( ) {
_ = stdoutReader . Close ( )
_ = stdoutWriter . Close ( )
} ( )
// This use of ... is safe as force-pushes have already been ruled out.
err = git . NewCommand ( "diff" , "--name-only" , oldCommitID + "..." + newCommitID ) .
RunInDirTimeoutEnvFullPipelineFunc ( env , - 1 , repo . Path ,
stdoutWriter , nil , nil ,
func ( ctx context . Context , cancel context . CancelFunc ) error {
_ = stdoutWriter . Close ( )
scanner := bufio . NewScanner ( stdoutReader )
for scanner . Scan ( ) {
path := strings . TrimSpace ( scanner . Text ( ) )
if len ( path ) == 0 {
continue
}
lpath := strings . ToLower ( path )
for _ , pat := range patterns {
if pat . Match ( lpath ) {
cancel ( )
return models . ErrFilePathProtected {
Path : path ,
}
}
}
}
if err := scanner . Err ( ) ; err != nil {
return err
}
_ = stdoutReader . Close ( )
return err
} )
if err != nil && ! models . IsErrFilePathProtected ( err ) {
log . Error ( "Unable to check file protection for commits from %s to %s in %s: %v" , oldCommitID , newCommitID , repo . Path , err )
}
return err
}
func readAndVerifyCommitsFromShaReader ( input io . ReadCloser , repo * git . Repository , env [ ] string ) error {
func readAndVerifyCommitsFromShaReader ( input io . ReadCloser , repo * git . Repository , env [ ] string ) error {
scanner := bufio . NewScanner ( input )
scanner := bufio . NewScanner ( input )
for scanner . Scan ( ) {
for scanner . Scan ( ) {
@ -202,6 +154,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
private . GitQuarantinePath + "=" + opts . GitQuarantinePath )
private . GitQuarantinePath + "=" + opts . GitQuarantinePath )
}
}
// Iterate across the provided old commit IDs
for i := range opts . OldCommitIDs {
for i := range opts . OldCommitIDs {
oldCommitID := opts . OldCommitIDs [ i ]
oldCommitID := opts . OldCommitIDs [ i ]
newCommitID := opts . NewCommitIDs [ i ]
newCommitID := opts . NewCommitIDs [ i ]
@ -224,8 +177,17 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
} )
} )
return
return
}
}
if protectBranch != nil && protectBranch . IsProtected ( ) {
// detect and prevent deletion
// Allow pushes to non-protected branches
if protectBranch == nil || ! protectBranch . IsProtected ( ) {
continue
}
// 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 {
if newCommitID == git . EmptySHA {
log . Warn ( "Forbidden: Branch: %s in %-v is protected from deletion" , branchName , repo )
log . Warn ( "Forbidden: Branch: %s in %-v is protected from deletion" , branchName , repo )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
@ -234,7 +196,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
return
return
}
}
// detect force push
// 2. Disallow force pushes to protected branches
if git . EmptySHA != oldCommitID {
if git . EmptySHA != oldCommitID {
output , err := git . NewCommand ( "rev-list" , "--max-count=1" , oldCommitID , "^" + newCommitID ) . RunInDirWithEnv ( repo . RepoPath ( ) , env )
output , err := git . NewCommand ( "rev-list" , "--max-count=1" , oldCommitID , "^" + newCommitID ) . RunInDirWithEnv ( repo . RepoPath ( ) , env )
if err != nil {
if err != nil {
@ -253,7 +215,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
}
}
}
}
// R equire signed commits
// 3. Enforce r equire signed commits
if protectBranch . RequireSignedCommits {
if protectBranch . RequireSignedCommits {
err := verifyCommits ( oldCommitID , newCommitID , gitRepo , env )
err := verifyCommits ( oldCommitID , newCommitID , gitRepo , env )
if err != nil {
if err != nil {
@ -273,10 +235,15 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
}
}
}
}
// Detect Protected file pattern
// 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 ( )
globs := protectBranch . GetProtectedFilePatterns ( )
if len ( globs ) > 0 {
if len ( globs ) > 0 {
err := checkFileProtection ( oldCommitID , newCommitID , globs , gitRepo , env )
_ , err := pull_service . C heckFileProtection( oldCommitID , newCommitID , globs , 1 , env , gitRepo )
if err != nil {
if err != nil {
if ! models . IsErrFilePathProtected ( err ) {
if ! models . IsErrFilePathProtected ( err ) {
log . Error ( "Unable to check file protection for commits from %s to %s in %-v: %v" , oldCommitID , newCommitID , repo , err )
log . Error ( "Unable to check file protection for commits from %s to %s in %-v: %v" , oldCommitID , newCommitID , repo , err )
@ -285,23 +252,45 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
} )
} )
return
return
}
}
protectedFilePath := err . ( models . ErrFilePathProtected ) . Path
changedProtectedfiles = true
protectedFilePath = err . ( models . ErrFilePathProtected ) . Path
}
}
// 5. Check if the doer is allowed to push
canPush := false
if opts . IsDeployKey {
canPush = ! changedProtectedfiles && protectBranch . CanPush && ( ! protectBranch . EnableWhitelist || protectBranch . WhitelistDeployKeys )
} else {
canPush = ! changedProtectedfiles && protectBranch . CanUserPush ( opts . UserID )
}
// 6. If we're not allowed to push directly
if ! canPush {
// Is this is a merge from the UI/API?
if opts . ProtectedBranchID == 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 )
log . Warn ( "Forbidden: Branch: %s in %-v is protected from changing file %s" , branchName , repo , protectedFilePath )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
"err" : fmt . Sprintf ( "branch %s is protected from changing file %s" , branchName , protectedFilePath ) ,
"err" : fmt . Sprintf ( "branch %s is protected from changing file %s" , branchName , protectedFilePath ) ,
} )
} )
return
return
}
}
}
canPush := false
// Or we're simply not able to push to this protected branch
if opts . IsDeployKey {
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v" , opts . UserID , branchName , repo )
canPush = protectBranch . CanPush && ( ! protectBranch . EnableWhitelist || protectBranch . WhitelistDeployKeys )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
} else {
"err" : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
canPush = protectBranch . CanUserPush ( opts . UserID )
} )
return
}
}
if ! canPush && opts . ProtectedBranchID > 0 {
// 6b. Merge (from UI or API)
// Merge (from UI or API)
// Get the PR, user and permissions for the user in the repository
pr , err := models . GetPullRequestByID ( opts . ProtectedBranchID )
pr , err := models . GetPullRequestByID ( opts . ProtectedBranchID )
if err != nil {
if err != nil {
log . Error ( "Unable to get PullRequest %d Error: %v" , opts . ProtectedBranchID , err )
log . Error ( "Unable to get PullRequest %d Error: %v" , opts . ProtectedBranchID , err )
@ -326,6 +315,8 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
} )
} )
return
return
}
}
// Now check if the user is allowed to merge PRs for this repository
allowedMerge , err := pull_service . IsUserAllowedToMerge ( pr , perm , user )
allowedMerge , err := pull_service . IsUserAllowedToMerge ( pr , perm , user )
if err != nil {
if err != nil {
log . Error ( "Error calculating if allowed to merge: %v" , err )
log . Error ( "Error calculating if allowed to merge: %v" , err )
@ -334,6 +325,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
} )
} )
return
return
}
}
if ! allowedMerge {
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" , opts . UserID , branchName , repo , pr . Index )
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d" , opts . UserID , branchName , repo , pr . Index )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
@ -341,9 +333,23 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
} )
} )
return
return
}
}
// Check all status checks and reviews is ok, unless repo admin which can bypass this.
if ! perm . IsAdmin ( ) {
// If we're an admin for the repository we can ignore status checks, reviews and override protected files
if err := pull_service . CheckPRReadyToMerge ( pr ) ; err != nil {
if perm . IsAdmin ( ) {
continue
}
// 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 , map [ string ] interface { } {
"err" : fmt . Sprintf ( "branch %s is protected from changing file %s" , branchName , protectedFilePath ) ,
} )
return
}
// Check all status checks and reviews are ok
if err := pull_service . CheckPRReadyToMerge ( pr , true ) ; err != nil {
if models . IsErrNotAllowedToMerge ( err ) {
if models . IsErrNotAllowedToMerge ( err ) {
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" , opts . UserID , branchName , repo , pr . Index , err . Error ( ) )
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" , opts . UserID , branchName , repo , pr . Index , err . Error ( ) )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
@ -355,13 +361,6 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
ctx . JSON ( http . StatusInternalServerError , map [ string ] interface { } {
ctx . JSON ( http . StatusInternalServerError , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Unable to get status of pull request %d. Error: %v" , opts . ProtectedBranchID , err ) ,
"err" : fmt . Sprintf ( "Unable to get status of pull request %d. Error: %v" , opts . ProtectedBranchID , err ) ,
} )
} )
}
}
} else if ! canPush {
log . Warn ( "Forbidden: User %d is not allowed to push to protected branch: %s in %-v" , opts . UserID , branchName , repo )
ctx . JSON ( http . StatusForbidden , map [ string ] interface { } {
"err" : fmt . Sprintf ( "Not allowed to push to protected branch %s" , branchName ) ,
} )
return
return
}
}
}
}