code.gitea.io/gitea@v1.21.7/routers/private/hook_pre_receive.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package private
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  
    11  	"code.gitea.io/gitea/models"
    12  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    13  	git_model "code.gitea.io/gitea/models/git"
    14  	issues_model "code.gitea.io/gitea/models/issues"
    15  	perm_model "code.gitea.io/gitea/models/perm"
    16  	access_model "code.gitea.io/gitea/models/perm/access"
    17  	"code.gitea.io/gitea/models/unit"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	gitea_context "code.gitea.io/gitea/modules/context"
    20  	"code.gitea.io/gitea/modules/git"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/private"
    23  	"code.gitea.io/gitea/modules/web"
    24  	pull_service "code.gitea.io/gitea/services/pull"
    25  )
    26  
    27  type preReceiveContext struct {
    28  	*gitea_context.PrivateContext
    29  
    30  	// loadedPusher indicates that where the following information are loaded
    31  	loadedPusher        bool
    32  	user                *user_model.User // it's the org user if a DeployKey is used
    33  	userPerm            access_model.Permission
    34  	deployKeyAccessMode perm_model.AccessMode
    35  
    36  	canCreatePullRequest        bool
    37  	checkedCanCreatePullRequest bool
    38  
    39  	canWriteCode        bool
    40  	checkedCanWriteCode bool
    41  
    42  	protectedTags    []*git_model.ProtectedTag
    43  	gotProtectedTags bool
    44  
    45  	env []string
    46  
    47  	opts *private.HookOptions
    48  
    49  	branchName string
    50  }
    51  
    52  // CanWriteCode returns true if pusher can write code
    53  func (ctx *preReceiveContext) CanWriteCode() bool {
    54  	if !ctx.checkedCanWriteCode {
    55  		if !ctx.loadPusherAndPermission() {
    56  			return false
    57  		}
    58  		ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx, ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
    59  		ctx.checkedCanWriteCode = true
    60  	}
    61  	return ctx.canWriteCode
    62  }
    63  
    64  // AssertCanWriteCode returns true if pusher can write code
    65  func (ctx *preReceiveContext) AssertCanWriteCode() bool {
    66  	if !ctx.CanWriteCode() {
    67  		if ctx.Written() {
    68  			return false
    69  		}
    70  		ctx.JSON(http.StatusForbidden, private.Response{
    71  			UserMsg: "User permission denied for writing.",
    72  		})
    73  		return false
    74  	}
    75  	return true
    76  }
    77  
    78  // CanCreatePullRequest returns true if pusher can create pull requests
    79  func (ctx *preReceiveContext) CanCreatePullRequest() bool {
    80  	if !ctx.checkedCanCreatePullRequest {
    81  		if !ctx.loadPusherAndPermission() {
    82  			return false
    83  		}
    84  		ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests)
    85  		ctx.checkedCanCreatePullRequest = true
    86  	}
    87  	return ctx.canCreatePullRequest
    88  }
    89  
    90  // AssertCreatePullRequest returns true if can create pull requests
    91  func (ctx *preReceiveContext) AssertCreatePullRequest() bool {
    92  	if !ctx.CanCreatePullRequest() {
    93  		if ctx.Written() {
    94  			return false
    95  		}
    96  		ctx.JSON(http.StatusForbidden, private.Response{
    97  			UserMsg: "User permission denied for creating pull-request.",
    98  		})
    99  		return false
   100  	}
   101  	return true
   102  }
   103  
   104  // HookPreReceive checks whether a individual commit is acceptable
   105  func HookPreReceive(ctx *gitea_context.PrivateContext) {
   106  	opts := web.GetForm(ctx).(*private.HookOptions)
   107  
   108  	ourCtx := &preReceiveContext{
   109  		PrivateContext: ctx,
   110  		env:            generateGitEnv(opts), // Generate git environment for checking commits
   111  		opts:           opts,
   112  	}
   113  
   114  	// Iterate across the provided old commit IDs
   115  	for i := range opts.OldCommitIDs {
   116  		oldCommitID := opts.OldCommitIDs[i]
   117  		newCommitID := opts.NewCommitIDs[i]
   118  		refFullName := opts.RefFullNames[i]
   119  
   120  		switch {
   121  		case refFullName.IsBranch():
   122  			preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
   123  		case refFullName.IsTag():
   124  			preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName)
   125  		case git.SupportProcReceive && refFullName.IsFor():
   126  			preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
   127  		default:
   128  			ourCtx.AssertCanWriteCode()
   129  		}
   130  		if ctx.Written() {
   131  			return
   132  		}
   133  	}
   134  
   135  	ctx.PlainText(http.StatusOK, "ok")
   136  }
   137  
   138  func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
   139  	branchName := refFullName.BranchName()
   140  	ctx.branchName = branchName
   141  
   142  	if !ctx.AssertCanWriteCode() {
   143  		return
   144  	}
   145  
   146  	repo := ctx.Repo.Repository
   147  	gitRepo := ctx.Repo.GitRepo
   148  
   149  	if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
   150  		log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
   151  		ctx.JSON(http.StatusForbidden, private.Response{
   152  			UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName),
   153  		})
   154  		return
   155  	}
   156  
   157  	protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
   158  	if err != nil {
   159  		log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
   160  		ctx.JSON(http.StatusInternalServerError, private.Response{
   161  			Err: err.Error(),
   162  		})
   163  		return
   164  	}
   165  
   166  	// Allow pushes to non-protected branches
   167  	if protectBranch == nil {
   168  		return
   169  	}
   170  	protectBranch.Repo = repo
   171  
   172  	// This ref is a protected branch.
   173  	//
   174  	// First of all we need to enforce absolutely:
   175  	//
   176  	// 1. Detect and prevent deletion of the branch
   177  	if newCommitID == git.EmptySHA {
   178  		log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo)
   179  		ctx.JSON(http.StatusForbidden, private.Response{
   180  			UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName),
   181  		})
   182  		return
   183  	}
   184  
   185  	// 2. Disallow force pushes to protected branches
   186  	if git.EmptySHA != oldCommitID {
   187  		output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env})
   188  		if err != nil {
   189  			log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err)
   190  			ctx.JSON(http.StatusInternalServerError, private.Response{
   191  				Err: fmt.Sprintf("Fail to detect force push: %v", err),
   192  			})
   193  			return
   194  		} else if len(output) > 0 {
   195  			log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
   196  			ctx.JSON(http.StatusForbidden, private.Response{
   197  				UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName),
   198  			})
   199  			return
   200  
   201  		}
   202  	}
   203  
   204  	// 3. Enforce require signed commits
   205  	if protectBranch.RequireSignedCommits {
   206  		err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env)
   207  		if err != nil {
   208  			if !isErrUnverifiedCommit(err) {
   209  				log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
   210  				ctx.JSON(http.StatusInternalServerError, private.Response{
   211  					Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err),
   212  				})
   213  				return
   214  			}
   215  			unverifiedCommit := err.(*errUnverifiedCommit).sha
   216  			log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit)
   217  			ctx.JSON(http.StatusForbidden, private.Response{
   218  				UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit),
   219  			})
   220  			return
   221  		}
   222  	}
   223  
   224  	// Now there are several tests which can be overridden:
   225  	//
   226  	// 4. Check protected file patterns - this is overridable from the UI
   227  	changedProtectedfiles := false
   228  	protectedFilePath := ""
   229  
   230  	globs := protectBranch.GetProtectedFilePatterns()
   231  	if len(globs) > 0 {
   232  		_, err := pull_service.CheckFileProtection(gitRepo, oldCommitID, newCommitID, globs, 1, ctx.env)
   233  		if err != nil {
   234  			if !models.IsErrFilePathProtected(err) {
   235  				log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
   236  				ctx.JSON(http.StatusInternalServerError, private.Response{
   237  					Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
   238  				})
   239  				return
   240  			}
   241  
   242  			changedProtectedfiles = true
   243  			protectedFilePath = err.(models.ErrFilePathProtected).Path
   244  		}
   245  	}
   246  
   247  	// 5. Check if the doer is allowed to push
   248  	var canPush bool
   249  	if ctx.opts.DeployKeyID != 0 {
   250  		canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
   251  	} else {
   252  		user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
   253  		if err != nil {
   254  			log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
   255  			ctx.JSON(http.StatusInternalServerError, private.Response{
   256  				Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
   257  			})
   258  			return
   259  		}
   260  		canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
   261  	}
   262  
   263  	// 6. If we're not allowed to push directly
   264  	if !canPush {
   265  		// Is this is a merge from the UI/API?
   266  		if ctx.opts.PullRequestID == 0 {
   267  			// 6a. If we're not merging from the UI/API then there are two ways we got here:
   268  			//
   269  			// We are changing a protected file and we're not allowed to do that
   270  			if changedProtectedfiles {
   271  				log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
   272  				ctx.JSON(http.StatusForbidden, private.Response{
   273  					UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
   274  				})
   275  				return
   276  			}
   277  
   278  			// Allow commits that only touch unprotected files
   279  			globs := protectBranch.GetUnprotectedFilePatterns()
   280  			if len(globs) > 0 {
   281  				unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, oldCommitID, newCommitID, globs, ctx.env)
   282  				if err != nil {
   283  					log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
   284  					ctx.JSON(http.StatusInternalServerError, private.Response{
   285  						Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
   286  					})
   287  					return
   288  				}
   289  				if unprotectedFilesOnly {
   290  					// Commit only touches unprotected files, this is allowed
   291  					return
   292  				}
   293  			}
   294  
   295  			// Or we're simply not able to push to this protected branch
   296  			log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
   297  			ctx.JSON(http.StatusForbidden, private.Response{
   298  				UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
   299  			})
   300  			return
   301  		}
   302  		// 6b. Merge (from UI or API)
   303  
   304  		// Get the PR, user and permissions for the user in the repository
   305  		pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
   306  		if err != nil {
   307  			log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
   308  			ctx.JSON(http.StatusInternalServerError, private.Response{
   309  				Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err),
   310  			})
   311  			return
   312  		}
   313  
   314  		// although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below
   315  		if !ctx.loadPusherAndPermission() {
   316  			// if error occurs, loadPusherAndPermission had written the error response
   317  			return
   318  		}
   319  
   320  		// Now check if the user is allowed to merge PRs for this repository
   321  		// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
   322  		allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user)
   323  		if err != nil {
   324  			log.Error("Error calculating if allowed to merge: %v", err)
   325  			ctx.JSON(http.StatusInternalServerError, private.Response{
   326  				Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err),
   327  			})
   328  			return
   329  		}
   330  
   331  		if !allowedMerge {
   332  			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)
   333  			ctx.JSON(http.StatusForbidden, private.Response{
   334  				UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
   335  			})
   336  			return
   337  		}
   338  
   339  		// If we're an admin for the repository we can ignore status checks, reviews and override protected files
   340  		if ctx.userPerm.IsAdmin() {
   341  			return
   342  		}
   343  
   344  		// Now if we're not an admin - we can't overwrite protected files so fail now
   345  		if changedProtectedfiles {
   346  			log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
   347  			ctx.JSON(http.StatusForbidden, private.Response{
   348  				UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
   349  			})
   350  			return
   351  		}
   352  
   353  		// Check all status checks and reviews are ok
   354  		if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
   355  			if models.IsErrDisallowedToMerge(err) {
   356  				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())
   357  				ctx.JSON(http.StatusForbidden, private.Response{
   358  					UserMsg: 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()),
   359  				})
   360  				return
   361  			}
   362  			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)
   363  			ctx.JSON(http.StatusInternalServerError, private.Response{
   364  				Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err),
   365  			})
   366  			return
   367  		}
   368  	}
   369  }
   370  
   371  func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
   372  	if !ctx.AssertCanWriteCode() {
   373  		return
   374  	}
   375  
   376  	tagName := refFullName.TagName()
   377  
   378  	if !ctx.gotProtectedTags {
   379  		var err error
   380  		ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
   381  		if err != nil {
   382  			log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err)
   383  			ctx.JSON(http.StatusInternalServerError, private.Response{
   384  				Err: err.Error(),
   385  			})
   386  			return
   387  		}
   388  		ctx.gotProtectedTags = true
   389  	}
   390  
   391  	isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
   392  	if err != nil {
   393  		ctx.JSON(http.StatusInternalServerError, private.Response{
   394  			Err: err.Error(),
   395  		})
   396  		return
   397  	}
   398  	if !isAllowed {
   399  		log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository)
   400  		ctx.JSON(http.StatusForbidden, private.Response{
   401  			UserMsg: fmt.Sprintf("Tag %s is protected", tagName),
   402  		})
   403  		return
   404  	}
   405  }
   406  
   407  func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) {
   408  	if !ctx.AssertCreatePullRequest() {
   409  		return
   410  	}
   411  
   412  	if ctx.Repo.Repository.IsEmpty {
   413  		ctx.JSON(http.StatusForbidden, private.Response{
   414  			UserMsg: "Can't create pull request for an empty repository.",
   415  		})
   416  		return
   417  	}
   418  
   419  	if ctx.opts.IsWiki {
   420  		ctx.JSON(http.StatusForbidden, private.Response{
   421  			UserMsg: "Pull requests are not supported on the wiki.",
   422  		})
   423  		return
   424  	}
   425  
   426  	baseBranchName := refFullName.ForBranchName()
   427  
   428  	baseBranchExist := false
   429  	if ctx.Repo.GitRepo.IsBranchExist(baseBranchName) {
   430  		baseBranchExist = true
   431  	}
   432  
   433  	if !baseBranchExist {
   434  		for p, v := range baseBranchName {
   435  			if v == '/' && ctx.Repo.GitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
   436  				baseBranchExist = true
   437  				break
   438  			}
   439  		}
   440  	}
   441  
   442  	if !baseBranchExist {
   443  		ctx.JSON(http.StatusForbidden, private.Response{
   444  			UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName),
   445  		})
   446  		return
   447  	}
   448  }
   449  
   450  func generateGitEnv(opts *private.HookOptions) (env []string) {
   451  	env = os.Environ()
   452  	if opts.GitAlternativeObjectDirectories != "" {
   453  		env = append(env,
   454  			private.GitAlternativeObjectDirectories+"="+opts.GitAlternativeObjectDirectories)
   455  	}
   456  	if opts.GitObjectDirectory != "" {
   457  		env = append(env,
   458  			private.GitObjectDirectory+"="+opts.GitObjectDirectory)
   459  	}
   460  	if opts.GitQuarantinePath != "" {
   461  		env = append(env,
   462  			private.GitQuarantinePath+"="+opts.GitQuarantinePath)
   463  	}
   464  	return env
   465  }
   466  
   467  // loadPusherAndPermission returns false if an error occurs, and it writes the error response
   468  func (ctx *preReceiveContext) loadPusherAndPermission() bool {
   469  	if ctx.loadedPusher {
   470  		return true
   471  	}
   472  
   473  	if ctx.opts.UserID == user_model.ActionsUserID {
   474  		ctx.user = user_model.NewActionsUser()
   475  		ctx.userPerm.AccessMode = perm_model.AccessMode(ctx.opts.ActionPerm)
   476  		if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil {
   477  			log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
   478  			ctx.JSON(http.StatusInternalServerError, private.Response{
   479  				Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err),
   480  			})
   481  			return false
   482  		}
   483  		ctx.userPerm.Units = ctx.Repo.Repository.Units
   484  		ctx.userPerm.UnitsMode = make(map[unit.Type]perm_model.AccessMode)
   485  		for _, u := range ctx.Repo.Repository.Units {
   486  			ctx.userPerm.UnitsMode[u.Type] = ctx.userPerm.AccessMode
   487  		}
   488  	} else {
   489  		user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
   490  		if err != nil {
   491  			log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
   492  			ctx.JSON(http.StatusInternalServerError, private.Response{
   493  				Err: fmt.Sprintf("Unable to get User id %d Error: %v", ctx.opts.UserID, err),
   494  			})
   495  			return false
   496  		}
   497  		ctx.user = user
   498  		userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user)
   499  		if err != nil {
   500  			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)
   501  			ctx.JSON(http.StatusInternalServerError, private.Response{
   502  				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),
   503  			})
   504  			return false
   505  		}
   506  		ctx.userPerm = userPerm
   507  	}
   508  
   509  	if ctx.opts.DeployKeyID != 0 {
   510  		deployKey, err := asymkey_model.GetDeployKeyByID(ctx, ctx.opts.DeployKeyID)
   511  		if err != nil {
   512  			log.Error("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err)
   513  			ctx.JSON(http.StatusInternalServerError, private.Response{
   514  				Err: fmt.Sprintf("Unable to get DeployKey id %d Error: %v", ctx.opts.DeployKeyID, err),
   515  			})
   516  			return false
   517  		}
   518  		ctx.deployKeyAccessMode = deployKey.Mode
   519  	}
   520  
   521  	ctx.loadedPusher = true
   522  	return true
   523  }