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.

345 lines
9.0 KiB

9 years ago
9 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.
5 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.
5 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.
5 years ago
  1. // Copyright 2015 The Gogs 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 models
  5. import (
  6. "fmt"
  7. "net/url"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/sync"
  14. "github.com/unknwon/com"
  15. )
  16. var (
  17. reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
  18. wikiWorkingPool = sync.NewExclusivePool()
  19. )
  20. // NormalizeWikiName normalizes a wiki name
  21. func NormalizeWikiName(name string) string {
  22. return strings.Replace(name, "-", " ", -1)
  23. }
  24. // WikiNameToSubURL converts a wiki name to its corresponding sub-URL.
  25. func WikiNameToSubURL(name string) string {
  26. return url.QueryEscape(strings.Replace(name, " ", "-", -1))
  27. }
  28. // WikiNameToFilename converts a wiki name to its corresponding filename.
  29. func WikiNameToFilename(name string) string {
  30. name = strings.Replace(name, " ", "-", -1)
  31. return url.QueryEscape(name) + ".md"
  32. }
  33. // WikiFilenameToName converts a wiki filename to its corresponding page name.
  34. func WikiFilenameToName(filename string) (string, error) {
  35. if !strings.HasSuffix(filename, ".md") {
  36. return "", ErrWikiInvalidFileName{filename}
  37. }
  38. basename := filename[:len(filename)-3]
  39. unescaped, err := url.QueryUnescape(basename)
  40. if err != nil {
  41. return "", err
  42. }
  43. return NormalizeWikiName(unescaped), nil
  44. }
  45. // WikiCloneLink returns clone URLs of repository wiki.
  46. func (repo *Repository) WikiCloneLink() *CloneLink {
  47. return repo.cloneLink(x, true)
  48. }
  49. // WikiPath returns wiki data path by given user and repository name.
  50. func WikiPath(userName, repoName string) string {
  51. return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".wiki.git")
  52. }
  53. // WikiPath returns wiki data path for given repository.
  54. func (repo *Repository) WikiPath() string {
  55. return WikiPath(repo.MustOwnerName(), repo.Name)
  56. }
  57. // HasWiki returns true if repository has wiki.
  58. func (repo *Repository) HasWiki() bool {
  59. return com.IsDir(repo.WikiPath())
  60. }
  61. // InitWiki initializes a wiki for repository,
  62. // it does nothing when repository already has wiki.
  63. func (repo *Repository) InitWiki() error {
  64. if repo.HasWiki() {
  65. return nil
  66. }
  67. if err := git.InitRepository(repo.WikiPath(), true); err != nil {
  68. return fmt.Errorf("InitRepository: %v", err)
  69. } else if err = createDelegateHooks(repo.WikiPath()); err != nil {
  70. return fmt.Errorf("createDelegateHooks: %v", err)
  71. }
  72. return nil
  73. }
  74. // nameAllowed checks if a wiki name is allowed
  75. func nameAllowed(name string) error {
  76. for _, reservedName := range reservedWikiNames {
  77. if name == reservedName {
  78. return ErrWikiReservedName{name}
  79. }
  80. }
  81. return nil
  82. }
  83. // updateWikiPage adds a new page to the repository wiki.
  84. func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
  85. if err = nameAllowed(newWikiName); err != nil {
  86. return err
  87. }
  88. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  89. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  90. if err = repo.InitWiki(); err != nil {
  91. return fmt.Errorf("InitWiki: %v", err)
  92. }
  93. hasMasterBranch := git.IsBranchExist(repo.WikiPath(), "master")
  94. basePath, err := CreateTemporaryPath("update-wiki")
  95. if err != nil {
  96. return err
  97. }
  98. defer func() {
  99. if err := RemoveTemporaryPath(basePath); err != nil {
  100. log.Error("Merge: RemoveTemporaryPath: %s", err)
  101. }
  102. }()
  103. cloneOpts := git.CloneRepoOptions{
  104. Bare: true,
  105. Shared: true,
  106. }
  107. if hasMasterBranch {
  108. cloneOpts.Branch = "master"
  109. }
  110. if err := git.Clone(repo.WikiPath(), basePath, cloneOpts); err != nil {
  111. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  112. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  113. }
  114. gitRepo, err := git.OpenRepository(basePath)
  115. if err != nil {
  116. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  117. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  118. }
  119. defer gitRepo.Close()
  120. if hasMasterBranch {
  121. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  122. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  123. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  124. }
  125. }
  126. newWikiPath := WikiNameToFilename(newWikiName)
  127. if isNew {
  128. filesInIndex, err := gitRepo.LsFiles(newWikiPath)
  129. if err != nil {
  130. log.Error("%v", err)
  131. return err
  132. }
  133. for _, file := range filesInIndex {
  134. if file == newWikiPath {
  135. return ErrWikiAlreadyExist{newWikiPath}
  136. }
  137. }
  138. } else {
  139. oldWikiPath := WikiNameToFilename(oldWikiName)
  140. filesInIndex, err := gitRepo.LsFiles(oldWikiPath)
  141. if err != nil {
  142. log.Error("%v", err)
  143. return err
  144. }
  145. found := false
  146. for _, file := range filesInIndex {
  147. if file == oldWikiPath {
  148. found = true
  149. break
  150. }
  151. }
  152. if found {
  153. err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
  154. if err != nil {
  155. log.Error("%v", err)
  156. return err
  157. }
  158. }
  159. }
  160. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  161. objectHash, err := gitRepo.HashObject(strings.NewReader(content))
  162. if err != nil {
  163. log.Error("%v", err)
  164. return err
  165. }
  166. if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
  167. log.Error("%v", err)
  168. return err
  169. }
  170. tree, err := gitRepo.WriteTree()
  171. if err != nil {
  172. log.Error("%v", err)
  173. return err
  174. }
  175. commitTreeOpts := git.CommitTreeOpts{
  176. Message: message,
  177. }
  178. sign, signingKey := repo.SignWikiCommit(doer)
  179. if sign {
  180. commitTreeOpts.KeyID = signingKey
  181. } else {
  182. commitTreeOpts.NoGPGSign = true
  183. }
  184. if hasMasterBranch {
  185. commitTreeOpts.Parents = []string{"HEAD"}
  186. }
  187. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  188. if err != nil {
  189. log.Error("%v", err)
  190. return err
  191. }
  192. if err := git.Push(basePath, git.PushOptions{
  193. Remote: "origin",
  194. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  195. Env: FullPushingEnvironment(
  196. doer,
  197. doer,
  198. repo,
  199. repo.Name+".wiki",
  200. 0,
  201. ),
  202. }); err != nil {
  203. log.Error("%v", err)
  204. return fmt.Errorf("Push: %v", err)
  205. }
  206. return nil
  207. }
  208. // AddWikiPage adds a new wiki page with a given wikiPath.
  209. func (repo *Repository) AddWikiPage(doer *User, wikiName, content, message string) error {
  210. return repo.updateWikiPage(doer, "", wikiName, content, message, true)
  211. }
  212. // EditWikiPage updates a wiki page identified by its wikiPath,
  213. // optionally also changing wikiPath.
  214. func (repo *Repository) EditWikiPage(doer *User, oldWikiName, newWikiName, content, message string) error {
  215. return repo.updateWikiPage(doer, oldWikiName, newWikiName, content, message, false)
  216. }
  217. // DeleteWikiPage deletes a wiki page identified by its path.
  218. func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) {
  219. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  220. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  221. if err = repo.InitWiki(); err != nil {
  222. return fmt.Errorf("InitWiki: %v", err)
  223. }
  224. basePath, err := CreateTemporaryPath("update-wiki")
  225. if err != nil {
  226. return err
  227. }
  228. defer func() {
  229. if err := RemoveTemporaryPath(basePath); err != nil {
  230. log.Error("Merge: RemoveTemporaryPath: %s", err)
  231. }
  232. }()
  233. if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
  234. Bare: true,
  235. Shared: true,
  236. Branch: "master",
  237. }); err != nil {
  238. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  239. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  240. }
  241. gitRepo, err := git.OpenRepository(basePath)
  242. if err != nil {
  243. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  244. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  245. }
  246. defer gitRepo.Close()
  247. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  248. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  249. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  250. }
  251. wikiPath := WikiNameToFilename(wikiName)
  252. filesInIndex, err := gitRepo.LsFiles(wikiPath)
  253. found := false
  254. for _, file := range filesInIndex {
  255. if file == wikiPath {
  256. found = true
  257. break
  258. }
  259. }
  260. if found {
  261. err := gitRepo.RemoveFilesFromIndex(wikiPath)
  262. if err != nil {
  263. return err
  264. }
  265. } else {
  266. return os.ErrNotExist
  267. }
  268. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  269. tree, err := gitRepo.WriteTree()
  270. if err != nil {
  271. return err
  272. }
  273. message := "Delete page '" + wikiName + "'"
  274. commitTreeOpts := git.CommitTreeOpts{
  275. Message: message,
  276. Parents: []string{"HEAD"},
  277. }
  278. sign, signingKey := repo.SignWikiCommit(doer)
  279. if sign {
  280. commitTreeOpts.KeyID = signingKey
  281. } else {
  282. commitTreeOpts.NoGPGSign = true
  283. }
  284. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  285. if err != nil {
  286. return err
  287. }
  288. if err := git.Push(basePath, git.PushOptions{
  289. Remote: "origin",
  290. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  291. Env: PushingEnvironment(doer, repo),
  292. }); err != nil {
  293. return fmt.Errorf("Push: %v", err)
  294. }
  295. return nil
  296. }