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.

200 lines
6.3 KiB

Sign merges, CRUD, Wiki and Repository initialisation with gpg key (#7631) This PR fixes #7598 by providing a configurable way of signing commits across the Gitea instance. Per repository configurability and import/generation of trusted secure keys is not provided by this PR - from a security PoV that's probably impossible to do properly. Similarly web-signing, that is asking the user to sign something, is not implemented - this could be done at a later stage however. ## Features - [x] If commit.gpgsign is set in .gitconfig sign commits and files created through repofiles. (merges should already have been signed.) - [x] Verify commits signed with the default gpg as valid - [x] Signer, Committer and Author can all be different - [x] Allow signer to be arbitrarily different - We still require the key to have an activated email on Gitea. A more complete implementation would be to use a keyserver and mark external-or-unactivated with an "unknown" trust level icon. - [x] Add a signing-key.gpg endpoint to get the default gpg pub key if available - Rather than add a fake web-flow user I've added this as an endpoint on /api/v1/signing-key.gpg - [x] Try to match the default key with a user on gitea - this is done at verification time - [x] Make things configurable? - app.ini configuration done - [x] when checking commits are signed need to check if they're actually verifiable too - [x] Add documentation I have decided that adjusting the docker to create a default gpg key is not the correct thing to do and therefore have not implemented this.
5 years ago
  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. package integrations
  5. import (
  6. "net/url"
  7. "testing"
  8. "code.gitea.io/gitea/models"
  9. "code.gitea.io/gitea/modules/repofiles"
  10. api "code.gitea.io/gitea/modules/structs"
  11. "code.gitea.io/gitea/modules/test"
  12. "github.com/stretchr/testify/assert"
  13. )
  14. func getDeleteRepoFileOptions(repo *models.Repository) *repofiles.DeleteRepoFileOptions {
  15. return &repofiles.DeleteRepoFileOptions{
  16. LastCommitID: "",
  17. OldBranch: repo.DefaultBranch,
  18. NewBranch: repo.DefaultBranch,
  19. TreePath: "README.md",
  20. Message: "Deletes README.md",
  21. SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
  22. Author: &repofiles.IdentityOptions{
  23. Name: "Bob Smith",
  24. Email: "bob@smith.com",
  25. },
  26. Committer: nil,
  27. }
  28. }
  29. func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
  30. // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
  31. return &api.FileResponse{
  32. Content: nil,
  33. Commit: &api.FileCommitResponse{
  34. Author: &api.CommitUser{
  35. Identity: api.Identity{
  36. Name: "Bob Smith",
  37. Email: "bob@smith.com",
  38. },
  39. },
  40. Committer: &api.CommitUser{
  41. Identity: api.Identity{
  42. Name: "Bob Smith",
  43. Email: "bob@smith.com",
  44. },
  45. },
  46. Message: "Deletes README.md\n",
  47. },
  48. Verification: &api.PayloadCommitVerification{
  49. Verified: false,
  50. Reason: "gpg.error.not_signed_commit",
  51. Signature: "",
  52. Payload: "",
  53. },
  54. }
  55. }
  56. func TestDeleteRepoFile(t *testing.T) {
  57. onGiteaRun(t, testDeleteRepoFile)
  58. }
  59. func testDeleteRepoFile(t *testing.T, u *url.URL) {
  60. // setup
  61. models.PrepareTestEnv(t)
  62. ctx := test.MockContext(t, "user2/repo1")
  63. ctx.SetParams(":id", "1")
  64. test.LoadRepo(t, ctx, 1)
  65. test.LoadRepoCommit(t, ctx)
  66. test.LoadUser(t, ctx, 2)
  67. test.LoadGitRepo(t, ctx)
  68. defer ctx.Repo.GitRepo.Close()
  69. repo := ctx.Repo.Repository
  70. doer := ctx.User
  71. opts := getDeleteRepoFileOptions(repo)
  72. t.Run("Delete README.md file", func(t *testing.T) {
  73. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  74. assert.NoError(t, err)
  75. expectedFileResponse := getExpectedDeleteFileResponse(u)
  76. assert.NotNil(t, fileResponse)
  77. assert.Nil(t, fileResponse.Content)
  78. assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
  79. assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
  80. assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
  81. assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
  82. })
  83. t.Run("Verify README.md has been deleted", func(t *testing.T) {
  84. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  85. assert.Nil(t, fileResponse)
  86. expectedError := "repository file does not exist [path: " + opts.TreePath + "]"
  87. assert.EqualError(t, err, expectedError)
  88. })
  89. }
  90. // Test opts with branch names removed, same results
  91. func TestDeleteRepoFileWithoutBranchNames(t *testing.T) {
  92. onGiteaRun(t, testDeleteRepoFileWithoutBranchNames)
  93. }
  94. func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) {
  95. // setup
  96. models.PrepareTestEnv(t)
  97. ctx := test.MockContext(t, "user2/repo1")
  98. ctx.SetParams(":id", "1")
  99. test.LoadRepo(t, ctx, 1)
  100. test.LoadRepoCommit(t, ctx)
  101. test.LoadUser(t, ctx, 2)
  102. test.LoadGitRepo(t, ctx)
  103. defer ctx.Repo.GitRepo.Close()
  104. repo := ctx.Repo.Repository
  105. doer := ctx.User
  106. opts := getDeleteRepoFileOptions(repo)
  107. opts.OldBranch = ""
  108. opts.NewBranch = ""
  109. t.Run("Delete README.md without Branch Name", func(t *testing.T) {
  110. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  111. assert.NoError(t, err)
  112. expectedFileResponse := getExpectedDeleteFileResponse(u)
  113. assert.NotNil(t, fileResponse)
  114. assert.Nil(t, fileResponse.Content)
  115. assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
  116. assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
  117. assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
  118. assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
  119. })
  120. }
  121. func TestDeleteRepoFileErrors(t *testing.T) {
  122. // setup
  123. models.PrepareTestEnv(t)
  124. ctx := test.MockContext(t, "user2/repo1")
  125. ctx.SetParams(":id", "1")
  126. test.LoadRepo(t, ctx, 1)
  127. test.LoadRepoCommit(t, ctx)
  128. test.LoadUser(t, ctx, 2)
  129. test.LoadGitRepo(t, ctx)
  130. defer ctx.Repo.GitRepo.Close()
  131. repo := ctx.Repo.Repository
  132. doer := ctx.User
  133. t.Run("Bad branch", func(t *testing.T) {
  134. opts := getDeleteRepoFileOptions(repo)
  135. opts.OldBranch = "bad_branch"
  136. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  137. assert.Error(t, err)
  138. assert.Nil(t, fileResponse)
  139. expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
  140. assert.EqualError(t, err, expectedError)
  141. })
  142. t.Run("Bad SHA", func(t *testing.T) {
  143. opts := getDeleteRepoFileOptions(repo)
  144. origSHA := opts.SHA
  145. opts.SHA = "bad_sha"
  146. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  147. assert.Nil(t, fileResponse)
  148. assert.Error(t, err)
  149. expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
  150. assert.EqualError(t, err, expectedError)
  151. })
  152. t.Run("New branch already exists", func(t *testing.T) {
  153. opts := getDeleteRepoFileOptions(repo)
  154. opts.NewBranch = "develop"
  155. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  156. assert.Nil(t, fileResponse)
  157. assert.Error(t, err)
  158. expectedError := "branch already exists [name: " + opts.NewBranch + "]"
  159. assert.EqualError(t, err, expectedError)
  160. })
  161. t.Run("TreePath is empty:", func(t *testing.T) {
  162. opts := getDeleteRepoFileOptions(repo)
  163. opts.TreePath = ""
  164. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  165. assert.Nil(t, fileResponse)
  166. assert.Error(t, err)
  167. expectedError := "path contains a malformed path component [path: ]"
  168. assert.EqualError(t, err, expectedError)
  169. })
  170. t.Run("TreePath is a git directory:", func(t *testing.T) {
  171. opts := getDeleteRepoFileOptions(repo)
  172. opts.TreePath = ".git"
  173. fileResponse, err := repofiles.DeleteRepoFile(repo, doer, opts)
  174. assert.Nil(t, fileResponse)
  175. assert.Error(t, err)
  176. expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
  177. assert.EqualError(t, err, expectedError)
  178. })
  179. }