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.

repofiles_update_test.go 13KB

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.
4 years ago
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.
4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. "path/filepath"
  8. "testing"
  9. "time"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/repofiles"
  13. "code.gitea.io/gitea/modules/setting"
  14. api "code.gitea.io/gitea/modules/structs"
  15. "code.gitea.io/gitea/modules/test"
  16. "github.com/stretchr/testify/assert"
  17. )
  18. func getCreateRepoFileOptions(repo *models.Repository) *repofiles.UpdateRepoFileOptions {
  19. return &repofiles.UpdateRepoFileOptions{
  20. OldBranch: repo.DefaultBranch,
  21. NewBranch: repo.DefaultBranch,
  22. TreePath: "new/file.txt",
  23. Message: "Creates new/file.txt",
  24. Content: "This is a NEW file",
  25. IsNewFile: true,
  26. Author: nil,
  27. Committer: nil,
  28. }
  29. }
  30. func getUpdateRepoFileOptions(repo *models.Repository) *repofiles.UpdateRepoFileOptions {
  31. return &repofiles.UpdateRepoFileOptions{
  32. OldBranch: repo.DefaultBranch,
  33. NewBranch: repo.DefaultBranch,
  34. TreePath: "README.md",
  35. Message: "Updates README.md",
  36. SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
  37. Content: "This is UPDATED content for the README file",
  38. IsNewFile: false,
  39. Author: nil,
  40. Committer: nil,
  41. }
  42. }
  43. func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileResponse {
  44. treePath := "new/file.txt"
  45. encoding := "base64"
  46. content := "VGhpcyBpcyBhIE5FVyBmaWxl"
  47. selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
  48. htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
  49. gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885"
  50. downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
  51. return &api.FileResponse{
  52. Content: &api.ContentsResponse{
  53. Name: filepath.Base(treePath),
  54. Path: treePath,
  55. SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
  56. Type: "file",
  57. Size: 18,
  58. Encoding: &encoding,
  59. Content: &content,
  60. URL: &selfURL,
  61. HTMLURL: &htmlURL,
  62. GitURL: &gitURL,
  63. DownloadURL: &downloadURL,
  64. Links: &api.FileLinksResponse{
  65. Self: &selfURL,
  66. GitURL: &gitURL,
  67. HTMLURL: &htmlURL,
  68. },
  69. },
  70. Commit: &api.FileCommitResponse{
  71. CommitMeta: api.CommitMeta{
  72. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
  73. SHA: commitID,
  74. },
  75. HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
  76. Author: &api.CommitUser{
  77. Identity: api.Identity{
  78. Name: "User Two",
  79. Email: "user2@noreply.example.org",
  80. },
  81. Date: time.Now().UTC().Format(time.RFC3339),
  82. },
  83. Committer: &api.CommitUser{
  84. Identity: api.Identity{
  85. Name: "User Two",
  86. Email: "user2@noreply.example.org",
  87. },
  88. Date: time.Now().UTC().Format(time.RFC3339),
  89. },
  90. Parents: []*api.CommitMeta{
  91. {
  92. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
  93. SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
  94. },
  95. },
  96. Message: "Updates README.md\n",
  97. Tree: &api.CommitMeta{
  98. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
  99. SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc",
  100. },
  101. },
  102. Verification: &api.PayloadCommitVerification{
  103. Verified: false,
  104. Reason: "gpg.error.not_signed_commit",
  105. Signature: "",
  106. Payload: "",
  107. },
  108. }
  109. }
  110. func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.FileResponse {
  111. encoding := "base64"
  112. content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ=="
  113. selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master"
  114. htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + filename
  115. gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647"
  116. downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename
  117. return &api.FileResponse{
  118. Content: &api.ContentsResponse{
  119. Name: filename,
  120. Path: filename,
  121. SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647",
  122. Type: "file",
  123. Size: 43,
  124. Encoding: &encoding,
  125. Content: &content,
  126. URL: &selfURL,
  127. HTMLURL: &htmlURL,
  128. GitURL: &gitURL,
  129. DownloadURL: &downloadURL,
  130. Links: &api.FileLinksResponse{
  131. Self: &selfURL,
  132. GitURL: &gitURL,
  133. HTMLURL: &htmlURL,
  134. },
  135. },
  136. Commit: &api.FileCommitResponse{
  137. CommitMeta: api.CommitMeta{
  138. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
  139. SHA: commitID,
  140. },
  141. HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
  142. Author: &api.CommitUser{
  143. Identity: api.Identity{
  144. Name: "User Two",
  145. Email: "user2@noreply.example.org",
  146. },
  147. Date: time.Now().UTC().Format(time.RFC3339),
  148. },
  149. Committer: &api.CommitUser{
  150. Identity: api.Identity{
  151. Name: "User Two",
  152. Email: "user2@noreply.example.org",
  153. },
  154. Date: time.Now().UTC().Format(time.RFC3339),
  155. },
  156. Parents: []*api.CommitMeta{
  157. {
  158. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
  159. SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
  160. },
  161. },
  162. Message: "Updates README.md\n",
  163. Tree: &api.CommitMeta{
  164. URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
  165. SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
  166. },
  167. },
  168. Verification: &api.PayloadCommitVerification{
  169. Verified: false,
  170. Reason: "gpg.error.not_signed_commit",
  171. Signature: "",
  172. Payload: "",
  173. },
  174. }
  175. }
  176. func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
  177. // setup
  178. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  179. ctx := test.MockContext(t, "user2/repo1")
  180. ctx.SetParams(":id", "1")
  181. test.LoadRepo(t, ctx, 1)
  182. test.LoadRepoCommit(t, ctx)
  183. test.LoadUser(t, ctx, 2)
  184. test.LoadGitRepo(t, ctx)
  185. repo := ctx.Repo.Repository
  186. doer := ctx.User
  187. opts := getCreateRepoFileOptions(repo)
  188. // test
  189. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  190. // asserts
  191. assert.Nil(t, err)
  192. gitRepo, _ := git.OpenRepository(repo.RepoPath())
  193. commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
  194. expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID)
  195. assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
  196. assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
  197. assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
  198. assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
  199. assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
  200. })
  201. }
  202. func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
  203. // setup
  204. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  205. ctx := test.MockContext(t, "user2/repo1")
  206. ctx.SetParams(":id", "1")
  207. test.LoadRepo(t, ctx, 1)
  208. test.LoadRepoCommit(t, ctx)
  209. test.LoadUser(t, ctx, 2)
  210. test.LoadGitRepo(t, ctx)
  211. repo := ctx.Repo.Repository
  212. doer := ctx.User
  213. opts := getUpdateRepoFileOptions(repo)
  214. // test
  215. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  216. // asserts
  217. assert.Nil(t, err)
  218. gitRepo, _ := git.OpenRepository(repo.RepoPath())
  219. commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
  220. expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath)
  221. assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
  222. assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
  223. assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
  224. assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
  225. assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
  226. })
  227. }
  228. func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
  229. // setup
  230. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  231. ctx := test.MockContext(t, "user2/repo1")
  232. ctx.SetParams(":id", "1")
  233. test.LoadRepo(t, ctx, 1)
  234. test.LoadRepoCommit(t, ctx)
  235. test.LoadUser(t, ctx, 2)
  236. test.LoadGitRepo(t, ctx)
  237. repo := ctx.Repo.Repository
  238. doer := ctx.User
  239. opts := getUpdateRepoFileOptions(repo)
  240. opts.FromTreePath = "README.md"
  241. opts.TreePath = "README_new.md" // new file name, README_new.md
  242. // test
  243. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  244. // asserts
  245. assert.Nil(t, err)
  246. gitRepo, _ := git.OpenRepository(repo.RepoPath())
  247. commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
  248. expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath)
  249. // assert that the old file no longer exists in the last commit of the branch
  250. fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
  251. toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
  252. assert.Nil(t, fromEntry) // Should no longer exist here
  253. assert.NotNil(t, toEntry) // Should exist here
  254. // assert SHA has remained the same but paths use the new file name
  255. assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
  256. assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
  257. assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
  258. assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
  259. assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
  260. assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
  261. })
  262. }
  263. // Test opts with branch names removed, should get same results as above test
  264. func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
  265. // setup
  266. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  267. ctx := test.MockContext(t, "user2/repo1")
  268. ctx.SetParams(":id", "1")
  269. test.LoadRepo(t, ctx, 1)
  270. test.LoadRepoCommit(t, ctx)
  271. test.LoadUser(t, ctx, 2)
  272. test.LoadGitRepo(t, ctx)
  273. repo := ctx.Repo.Repository
  274. doer := ctx.User
  275. opts := getUpdateRepoFileOptions(repo)
  276. opts.OldBranch = ""
  277. opts.NewBranch = ""
  278. // test
  279. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  280. // asserts
  281. assert.Nil(t, err)
  282. gitRepo, _ := git.OpenRepository(repo.RepoPath())
  283. commitID, _ := gitRepo.GetBranchCommitID(repo.DefaultBranch)
  284. expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath)
  285. assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
  286. })
  287. }
  288. func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
  289. // setup
  290. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  291. ctx := test.MockContext(t, "user2/repo1")
  292. ctx.SetParams(":id", "1")
  293. test.LoadRepo(t, ctx, 1)
  294. test.LoadRepoCommit(t, ctx)
  295. test.LoadUser(t, ctx, 2)
  296. test.LoadGitRepo(t, ctx)
  297. repo := ctx.Repo.Repository
  298. doer := ctx.User
  299. t.Run("bad branch", func(t *testing.T) {
  300. opts := getUpdateRepoFileOptions(repo)
  301. opts.OldBranch = "bad_branch"
  302. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  303. assert.Error(t, err)
  304. assert.Nil(t, fileResponse)
  305. expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
  306. assert.EqualError(t, err, expectedError)
  307. })
  308. t.Run("bad SHA", func(t *testing.T) {
  309. opts := getUpdateRepoFileOptions(repo)
  310. origSHA := opts.SHA
  311. opts.SHA = "bad_sha"
  312. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  313. assert.Nil(t, fileResponse)
  314. assert.Error(t, err)
  315. expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
  316. assert.EqualError(t, err, expectedError)
  317. })
  318. t.Run("new branch already exists", func(t *testing.T) {
  319. opts := getUpdateRepoFileOptions(repo)
  320. opts.NewBranch = "develop"
  321. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  322. assert.Nil(t, fileResponse)
  323. assert.Error(t, err)
  324. expectedError := "branch already exists [name: " + opts.NewBranch + "]"
  325. assert.EqualError(t, err, expectedError)
  326. })
  327. t.Run("treePath is empty:", func(t *testing.T) {
  328. opts := getUpdateRepoFileOptions(repo)
  329. opts.TreePath = ""
  330. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  331. assert.Nil(t, fileResponse)
  332. assert.Error(t, err)
  333. expectedError := "path contains a malformed path component [path: ]"
  334. assert.EqualError(t, err, expectedError)
  335. })
  336. t.Run("treePath is a git directory:", func(t *testing.T) {
  337. opts := getUpdateRepoFileOptions(repo)
  338. opts.TreePath = ".git"
  339. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  340. assert.Nil(t, fileResponse)
  341. assert.Error(t, err)
  342. expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
  343. assert.EqualError(t, err, expectedError)
  344. })
  345. t.Run("create file that already exists", func(t *testing.T) {
  346. opts := getCreateRepoFileOptions(repo)
  347. opts.TreePath = "README.md" //already exists
  348. fileResponse, err := repofiles.CreateOrUpdateRepoFile(repo, doer, opts)
  349. assert.Nil(t, fileResponse)
  350. assert.Error(t, err)
  351. expectedError := "repository file already exists [path: " + opts.TreePath + "]"
  352. assert.EqualError(t, err, expectedError)
  353. })
  354. })
  355. }