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