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 }