You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

hook_pre_receive.go 18KB

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