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.

351 lines
11 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.
4 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. "encoding/json"
  7. "fmt"
  8. "io/ioutil"
  9. "net/http"
  10. "testing"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/auth"
  13. api "code.gitea.io/gitea/modules/structs"
  14. "github.com/stretchr/testify/assert"
  15. )
  16. type APITestContext struct {
  17. Reponame string
  18. Session *TestSession
  19. Token string
  20. Username string
  21. ExpectedCode int
  22. }
  23. func NewAPITestContext(t *testing.T, username, reponame string) APITestContext {
  24. session := loginUser(t, username)
  25. token := getTokenForLoggedInUser(t, session)
  26. return APITestContext{
  27. Session: session,
  28. Token: token,
  29. Username: username,
  30. Reponame: reponame,
  31. }
  32. }
  33. func (ctx APITestContext) GitPath() string {
  34. return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame)
  35. }
  36. func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
  37. return func(t *testing.T) {
  38. createRepoOption := &api.CreateRepoOption{
  39. AutoInit: !empty,
  40. Description: "Temporary repo",
  41. Name: ctx.Reponame,
  42. Private: true,
  43. Gitignores: "",
  44. License: "WTFPL",
  45. Readme: "Default",
  46. }
  47. req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+ctx.Token, createRepoOption)
  48. if ctx.ExpectedCode != 0 {
  49. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  50. return
  51. }
  52. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  53. var repository api.Repository
  54. DecodeJSON(t, resp, &repository)
  55. if len(callback) > 0 {
  56. callback[0](t, repository)
  57. }
  58. }
  59. }
  60. func doAPIAddCollaborator(ctx APITestContext, username string, mode models.AccessMode) func(*testing.T) {
  61. return func(t *testing.T) {
  62. permission := "read"
  63. if mode == models.AccessModeAdmin {
  64. permission = "admin"
  65. } else if mode > models.AccessModeRead {
  66. permission = "write"
  67. }
  68. addCollaboratorOption := &api.AddCollaboratorOption{
  69. Permission: &permission,
  70. }
  71. req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s?token=%s", ctx.Username, ctx.Reponame, username, ctx.Token), addCollaboratorOption)
  72. if ctx.ExpectedCode != 0 {
  73. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  74. return
  75. }
  76. ctx.Session.MakeRequest(t, req, http.StatusNoContent)
  77. }
  78. }
  79. func doAPIForkRepository(ctx APITestContext, username string, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
  80. return func(t *testing.T) {
  81. createForkOption := &api.CreateForkOption{}
  82. req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks?token=%s", username, ctx.Reponame, ctx.Token), createForkOption)
  83. if ctx.ExpectedCode != 0 {
  84. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  85. return
  86. }
  87. resp := ctx.Session.MakeRequest(t, req, http.StatusAccepted)
  88. var repository api.Repository
  89. DecodeJSON(t, resp, &repository)
  90. if len(callback) > 0 {
  91. callback[0](t, repository)
  92. }
  93. }
  94. }
  95. func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
  96. return func(t *testing.T) {
  97. urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
  98. req := NewRequest(t, "GET", urlStr)
  99. if ctx.ExpectedCode != 0 {
  100. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  101. return
  102. }
  103. resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
  104. var repository api.Repository
  105. DecodeJSON(t, resp, &repository)
  106. if len(callback) > 0 {
  107. callback[0](t, repository)
  108. }
  109. }
  110. }
  111. func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) {
  112. return func(t *testing.T) {
  113. urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
  114. req := NewRequest(t, "DELETE", urlStr)
  115. if ctx.ExpectedCode != 0 {
  116. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  117. return
  118. }
  119. ctx.Session.MakeRequest(t, req, http.StatusNoContent)
  120. }
  121. }
  122. func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) {
  123. return func(t *testing.T) {
  124. urlStr := fmt.Sprintf("/api/v1/user/keys?token=%s", ctx.Token)
  125. dataPubKey, err := ioutil.ReadFile(keyFile + ".pub")
  126. assert.NoError(t, err)
  127. req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateKeyOption{
  128. Title: keyname,
  129. Key: string(dataPubKey),
  130. })
  131. if ctx.ExpectedCode != 0 {
  132. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  133. return
  134. }
  135. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  136. var publicKey api.PublicKey
  137. DecodeJSON(t, resp, &publicKey)
  138. if len(callback) > 0 {
  139. callback[0](t, publicKey)
  140. }
  141. }
  142. }
  143. func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) {
  144. return func(t *testing.T) {
  145. urlStr := fmt.Sprintf("/api/v1/user/keys/%d?token=%s", keyID, ctx.Token)
  146. req := NewRequest(t, "DELETE", urlStr)
  147. if ctx.ExpectedCode != 0 {
  148. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  149. return
  150. }
  151. ctx.Session.MakeRequest(t, req, http.StatusNoContent)
  152. }
  153. }
  154. func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) {
  155. return func(t *testing.T) {
  156. urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
  157. dataPubKey, err := ioutil.ReadFile(keyFile + ".pub")
  158. assert.NoError(t, err)
  159. req := NewRequestWithJSON(t, "POST", urlStr, api.CreateKeyOption{
  160. Title: keyname,
  161. Key: string(dataPubKey),
  162. ReadOnly: readOnly,
  163. })
  164. if ctx.ExpectedCode != 0 {
  165. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  166. return
  167. }
  168. ctx.Session.MakeRequest(t, req, http.StatusCreated)
  169. }
  170. }
  171. func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) {
  172. return func(t *testing.T) (api.PullRequest, error) {
  173. urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s",
  174. owner, repo, ctx.Token)
  175. req := NewRequestWithJSON(t, http.MethodPost, urlStr, &api.CreatePullRequestOption{
  176. Head: headBranch,
  177. Base: baseBranch,
  178. Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch),
  179. })
  180. expected := 201
  181. if ctx.ExpectedCode != 0 {
  182. expected = ctx.ExpectedCode
  183. }
  184. resp := ctx.Session.MakeRequest(t, req, expected)
  185. decoder := json.NewDecoder(resp.Body)
  186. pr := api.PullRequest{}
  187. err := decoder.Decode(&pr)
  188. return pr, err
  189. }
  190. }
  191. func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
  192. return func(t *testing.T) {
  193. urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
  194. owner, repo, index, ctx.Token)
  195. req := NewRequestWithJSON(t, http.MethodPost, urlStr, &auth.MergePullRequestForm{
  196. MergeMessageField: "doAPIMergePullRequest Merge",
  197. Do: string(models.MergeStyleMerge),
  198. })
  199. if ctx.ExpectedCode != 0 {
  200. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  201. return
  202. }
  203. ctx.Session.MakeRequest(t, req, 200)
  204. }
  205. }
  206. func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
  207. return func(t *testing.T) {
  208. req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
  209. if ctx.ExpectedCode != 0 {
  210. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  211. return
  212. }
  213. resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
  214. var branch api.Branch
  215. DecodeJSON(t, resp, &branch)
  216. if len(callback) > 0 {
  217. callback[0](t, branch)
  218. }
  219. }
  220. }
  221. func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
  222. return func(t *testing.T) {
  223. url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
  224. req := NewRequestWithJSON(t, "POST", url, &options)
  225. if ctx.ExpectedCode != 0 {
  226. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  227. return
  228. }
  229. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  230. var contents api.FileResponse
  231. DecodeJSON(t, resp, &contents)
  232. if len(callback) > 0 {
  233. callback[0](t, contents)
  234. }
  235. }
  236. }
  237. func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
  238. return func(t *testing.T) {
  239. url := fmt.Sprintf("/api/v1/orgs?token=%s", ctx.Token)
  240. req := NewRequestWithJSON(t, "POST", url, &options)
  241. if ctx.ExpectedCode != 0 {
  242. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  243. return
  244. }
  245. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  246. var contents api.Organization
  247. DecodeJSON(t, resp, &contents)
  248. if len(callback) > 0 {
  249. callback[0](t, contents)
  250. }
  251. }
  252. }
  253. func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
  254. return func(t *testing.T) {
  255. url := fmt.Sprintf("/api/v1/orgs/%s/repos?token=%s", orgName, ctx.Token)
  256. req := NewRequestWithJSON(t, "POST", url, &options)
  257. if ctx.ExpectedCode != 0 {
  258. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  259. return
  260. }
  261. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  262. var contents api.Repository
  263. DecodeJSON(t, resp, &contents)
  264. if len(callback) > 0 {
  265. callback[0](t, contents)
  266. }
  267. }
  268. }
  269. func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
  270. return func(t *testing.T) {
  271. url := fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, ctx.Token)
  272. req := NewRequestWithJSON(t, "POST", url, &options)
  273. if ctx.ExpectedCode != 0 {
  274. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  275. return
  276. }
  277. resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
  278. var contents api.Team
  279. DecodeJSON(t, resp, &contents)
  280. if len(callback) > 0 {
  281. callback[0](t, contents)
  282. }
  283. }
  284. }
  285. func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
  286. return func(t *testing.T) {
  287. url := fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", teamID, username, ctx.Token)
  288. req := NewRequest(t, "PUT", url)
  289. if ctx.ExpectedCode != 0 {
  290. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  291. return
  292. }
  293. ctx.Session.MakeRequest(t, req, http.StatusNoContent)
  294. }
  295. }
  296. func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
  297. return func(t *testing.T) {
  298. url := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s?token=%s", teamID, orgName, repoName, ctx.Token)
  299. req := NewRequest(t, "PUT", url)
  300. if ctx.ExpectedCode != 0 {
  301. ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
  302. return
  303. }
  304. ctx.Session.MakeRequest(t, req, http.StatusNoContent)
  305. }
  306. }