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

publickey.go 18 kB

11 years ago
11 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. // Copyright 2014 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. "bufio"
  7. "encoding/base64"
  8. "encoding/binary"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "io/ioutil"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "strings"
  17. "sync"
  18. "time"
  19. "github.com/Unknwon/com"
  20. "github.com/go-xorm/xorm"
  21. "github.com/gogits/gogs/modules/log"
  22. "github.com/gogits/gogs/modules/process"
  23. "github.com/gogits/gogs/modules/setting"
  24. )
  25. const (
  26. // "### autogenerated by gitgos, DO NOT EDIT\n"
  27. _TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
  28. )
  29. var (
  30. ErrKeyUnableVerify = errors.New("Unable to verify public key")
  31. )
  32. var sshOpLocker = sync.Mutex{}
  33. var SSHPath string // SSH directory.
  34. // homeDir returns the home directory of current user.
  35. func homeDir() string {
  36. home, err := com.HomeDir()
  37. if err != nil {
  38. log.Fatal(4, "Fail to get home directory: %v", err)
  39. }
  40. return home
  41. }
  42. func init() {
  43. // Determine and create .ssh path.
  44. SSHPath = filepath.Join(homeDir(), ".ssh")
  45. if err := os.MkdirAll(SSHPath, 0700); err != nil {
  46. log.Fatal(4, "fail to create '%s': %v", SSHPath, err)
  47. }
  48. }
  49. type KeyType int
  50. const (
  51. KEY_TYPE_USER = iota + 1
  52. KEY_TYPE_DEPLOY
  53. )
  54. // PublicKey represents a SSH or deploy key.
  55. type PublicKey struct {
  56. ID int64 `xorm:"pk autoincr"`
  57. OwnerID int64 `xorm:"INDEX NOT NULL"`
  58. Name string `xorm:"NOT NULL"`
  59. Fingerprint string `xorm:"NOT NULL"`
  60. Content string `xorm:"TEXT NOT NULL"`
  61. Mode AccessMode `xorm:"NOT NULL DEFAULT 2"`
  62. Type KeyType `xorm:"NOT NULL DEFAULT 1"`
  63. Created time.Time `xorm:"CREATED"`
  64. Updated time.Time // Note: Updated must below Created for AfterSet.
  65. HasRecentActivity bool `xorm:"-"`
  66. HasUsed bool `xorm:"-"`
  67. }
  68. func (k *PublicKey) AfterSet(colName string, _ xorm.Cell) {
  69. switch colName {
  70. case "created":
  71. k.HasUsed = k.Updated.After(k.Created)
  72. k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
  73. }
  74. }
  75. // OmitEmail returns content of public key but without e-mail address.
  76. func (k *PublicKey) OmitEmail() string {
  77. return strings.Join(strings.Split(k.Content, " ")[:2], " ")
  78. }
  79. // GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
  80. func (key *PublicKey) GetAuthorizedString() string {
  81. return fmt.Sprintf(_TPL_PUBLICK_KEY, setting.AppPath, key.ID, setting.CustomConf, key.Content)
  82. }
  83. func extractTypeFromBase64Key(key string) (string, error) {
  84. b, err := base64.StdEncoding.DecodeString(key)
  85. if err != nil || len(b) < 4 {
  86. return "", errors.New("Invalid key format")
  87. }
  88. keyLength := int(binary.BigEndian.Uint32(b))
  89. if len(b) < 4+keyLength {
  90. return "", errors.New("Invalid key format")
  91. }
  92. return string(b[4 : 4+keyLength]), nil
  93. }
  94. // parseKeyString parses any key string in openssh or ssh2 format to clean openssh string (rfc4253)
  95. func parseKeyString(content string) (string, error) {
  96. // Transform all legal line endings to a single "\n"
  97. s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1)
  98. lines := strings.Split(s, "\n")
  99. var keyType, keyContent, keyComment string
  100. if len(lines) == 1 {
  101. // Parse openssh format
  102. parts := strings.SplitN(lines[0], " ", 3)
  103. switch len(parts) {
  104. case 0:
  105. return "", errors.New("Empty key")
  106. case 1:
  107. keyContent = parts[0]
  108. case 2:
  109. keyType = parts[0]
  110. keyContent = parts[1]
  111. default:
  112. keyType = parts[0]
  113. keyContent = parts[1]
  114. keyComment = parts[2]
  115. }
  116. // If keyType is not given, extract it from content. If given, validate it
  117. if len(keyType) == 0 {
  118. if t, err := extractTypeFromBase64Key(keyContent); err == nil {
  119. keyType = t
  120. } else {
  121. return "", err
  122. }
  123. } else {
  124. if t, err := extractTypeFromBase64Key(keyContent); err != nil || keyType != t {
  125. return "", err
  126. }
  127. }
  128. } else {
  129. // Parse SSH2 file format.
  130. continuationLine := false
  131. for _, line := range lines {
  132. // Skip lines that:
  133. // 1) are a continuation of the previous line,
  134. // 2) contain ":" as that are comment lines
  135. // 3) contain "-" as that are begin and end tags
  136. if continuationLine || strings.ContainsAny(line, ":-") {
  137. continuationLine = strings.HasSuffix(line, "\\")
  138. } else {
  139. keyContent = keyContent + line
  140. }
  141. }
  142. if t, err := extractTypeFromBase64Key(keyContent); err == nil {
  143. keyType = t
  144. } else {
  145. return "", err
  146. }
  147. }
  148. return keyType + " " + keyContent + " " + keyComment, nil
  149. }
  150. // CheckPublicKeyString checks if the given public key string is recognized by SSH.
  151. func CheckPublicKeyString(content string) (_ string, err error) {
  152. content, err = parseKeyString(content)
  153. if err != nil {
  154. return "", err
  155. }
  156. content = strings.TrimRight(content, "\n\r")
  157. if strings.ContainsAny(content, "\n\r") {
  158. return "", errors.New("only a single line with a single key please")
  159. }
  160. // write the key to a file…
  161. tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest")
  162. if err != nil {
  163. return "", err
  164. }
  165. tmpPath := tmpFile.Name()
  166. defer os.Remove(tmpPath)
  167. tmpFile.WriteString(content)
  168. tmpFile.Close()
  169. // Check if ssh-keygen recognizes its contents.
  170. stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-lf", tmpPath)
  171. if err != nil {
  172. return "", errors.New("ssh-keygen -lf: " + stderr)
  173. } else if len(stdout) < 2 {
  174. return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
  175. }
  176. // The ssh-keygen in Windows does not print key type, so no need go further.
  177. if setting.IsWindows {
  178. return content, nil
  179. }
  180. sshKeygenOutput := strings.Split(stdout, " ")
  181. if len(sshKeygenOutput) < 4 {
  182. return content, ErrKeyUnableVerify
  183. }
  184. // Check if key type and key size match.
  185. if !setting.Service.DisableMinimumKeySizeCheck {
  186. keySize := com.StrTo(sshKeygenOutput[0]).MustInt()
  187. if keySize == 0 {
  188. return "", errors.New("cannot get key size of the given key")
  189. }
  190. keyType := strings.Trim(sshKeygenOutput[len(sshKeygenOutput)-1], " ()\n")
  191. if minimumKeySize := setting.Service.MinimumKeySizes[keyType]; minimumKeySize == 0 {
  192. return "", fmt.Errorf("unrecognized public key type: %s", keyType)
  193. } else if keySize < minimumKeySize {
  194. return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
  195. }
  196. }
  197. return content, nil
  198. }
  199. // saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
  200. func saveAuthorizedKeyFile(keys ...*PublicKey) error {
  201. sshOpLocker.Lock()
  202. defer sshOpLocker.Unlock()
  203. fpath := filepath.Join(SSHPath, "authorized_keys")
  204. f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  205. if err != nil {
  206. return err
  207. }
  208. defer f.Close()
  209. fi, err := f.Stat()
  210. if err != nil {
  211. return err
  212. }
  213. // FIXME: following command does not support in Windows.
  214. if !setting.IsWindows {
  215. // .ssh directory should have mode 700, and authorized_keys file should have mode 600.
  216. if fi.Mode().Perm() > 0600 {
  217. log.Error(4, "authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
  218. if err = f.Chmod(0600); err != nil {
  219. return err
  220. }
  221. }
  222. }
  223. for _, key := range keys {
  224. if _, err = f.WriteString(key.GetAuthorizedString()); err != nil {
  225. return err
  226. }
  227. }
  228. return nil
  229. }
  230. // checkKeyContent onlys checks if key content has been used as public key,
  231. // it is OK to use same key as deploy key for multiple repositories/users.
  232. func checkKeyContent(content string) error {
  233. has, err := x.Get(&PublicKey{
  234. Content: content,
  235. Type: KEY_TYPE_USER,
  236. })
  237. if err != nil {
  238. return err
  239. } else if has {
  240. return ErrKeyAlreadyExist{0, content}
  241. }
  242. return nil
  243. }
  244. func addKey(e Engine, key *PublicKey) (err error) {
  245. // Calculate fingerprint.
  246. tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
  247. "id_rsa.pub"), "\\", "/", -1)
  248. os.MkdirAll(path.Dir(tmpPath), os.ModePerm)
  249. if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
  250. return err
  251. }
  252. stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
  253. if err != nil {
  254. return errors.New("ssh-keygen -lf: " + stderr)
  255. } else if len(stdout) < 2 {
  256. return errors.New("not enough output for calculating fingerprint: " + stdout)
  257. }
  258. key.Fingerprint = strings.Split(stdout, " ")[1]
  259. // Save SSH key.
  260. if _, err = e.Insert(key); err != nil {
  261. return err
  262. }
  263. return saveAuthorizedKeyFile(key)
  264. }
  265. // AddPublicKey adds new public key to database and authorized_keys file.
  266. func AddPublicKey(ownerID int64, name, content string) (err error) {
  267. if err = checkKeyContent(content); err != nil {
  268. return err
  269. }
  270. // Key name of same user cannot be duplicated.
  271. has, err := x.Where("owner_id=? AND name=?", ownerID, name).Get(new(PublicKey))
  272. if err != nil {
  273. return err
  274. } else if has {
  275. return ErrKeyNameAlreadyUsed{ownerID, name}
  276. }
  277. sess := x.NewSession()
  278. defer sessionRelease(sess)
  279. if err = sess.Begin(); err != nil {
  280. return err
  281. }
  282. key := &PublicKey{
  283. OwnerID: ownerID,
  284. Name: name,
  285. Content: content,
  286. Mode: ACCESS_MODE_WRITE,
  287. Type: KEY_TYPE_USER,
  288. }
  289. if err = addKey(sess, key); err != nil {
  290. return fmt.Errorf("addKey: %v", err)
  291. }
  292. return sess.Commit()
  293. }
  294. // GetPublicKeyByID returns public key by given ID.
  295. func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
  296. key := new(PublicKey)
  297. has, err := x.Id(keyID).Get(key)
  298. if err != nil {
  299. return nil, err
  300. } else if !has {
  301. return nil, ErrKeyNotExist{keyID}
  302. }
  303. return key, nil
  304. }
  305. // SearchPublicKeyByContent searches content as prefix (leak e-mail part)
  306. // and returns public key found.
  307. func SearchPublicKeyByContent(content string) (*PublicKey, error) {
  308. key := new(PublicKey)
  309. has, err := x.Where("content like ?", content+"%").Get(key)
  310. if err != nil {
  311. return nil, err
  312. } else if !has {
  313. return nil, ErrKeyNotExist{}
  314. }
  315. return key, nil
  316. }
  317. // ListPublicKeys returns a list of public keys belongs to given user.
  318. func ListPublicKeys(uid int64) ([]*PublicKey, error) {
  319. keys := make([]*PublicKey, 0, 5)
  320. return keys, x.Where("owner_id=?", uid).Find(&keys)
  321. }
  322. // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file.
  323. func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error {
  324. fr, err := os.Open(p)
  325. if err != nil {
  326. return err
  327. }
  328. defer fr.Close()
  329. fw, err := os.OpenFile(tmpP, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
  330. if err != nil {
  331. return err
  332. }
  333. defer fw.Close()
  334. isFound := false
  335. keyword := fmt.Sprintf("key-%d", key.ID)
  336. buf := bufio.NewReader(fr)
  337. for {
  338. line, errRead := buf.ReadString('\n')
  339. line = strings.TrimSpace(line)
  340. if errRead != nil {
  341. if errRead != io.EOF {
  342. return errRead
  343. }
  344. // Reached end of file, if nothing to read then break,
  345. // otherwise handle the last line.
  346. if len(line) == 0 {
  347. break
  348. }
  349. }
  350. // Found the line and copy rest of file.
  351. if !isFound && strings.Contains(line, keyword) && strings.Contains(line, key.Content) {
  352. isFound = true
  353. continue
  354. }
  355. // Still finding the line, copy the line that currently read.
  356. if _, err = fw.WriteString(line + "\n"); err != nil {
  357. return err
  358. }
  359. if errRead == io.EOF {
  360. break
  361. }
  362. }
  363. return nil
  364. }
  365. // UpdatePublicKey updates given public key.
  366. func UpdatePublicKey(key *PublicKey) error {
  367. _, err := x.Id(key.ID).AllCols().Update(key)
  368. return err
  369. }
  370. func deletePublicKey(e *xorm.Session, keyID int64) error {
  371. sshOpLocker.Lock()
  372. defer sshOpLocker.Unlock()
  373. key := &PublicKey{ID: keyID}
  374. has, err := e.Get(key)
  375. if err != nil {
  376. return err
  377. } else if !has {
  378. return nil
  379. }
  380. if _, err = e.Id(key.ID).Delete(new(PublicKey)); err != nil {
  381. return err
  382. }
  383. fpath := filepath.Join(SSHPath, "authorized_keys")
  384. tmpPath := filepath.Join(SSHPath, "authorized_keys.tmp")
  385. if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil {
  386. return err
  387. } else if err = os.Remove(fpath); err != nil {
  388. return err
  389. }
  390. return os.Rename(tmpPath, fpath)
  391. }
  392. // DeletePublicKey deletes SSH key information both in database and authorized_keys file.
  393. func DeletePublicKey(id int64) (err error) {
  394. has, err := x.Id(id).Get(new(PublicKey))
  395. if err != nil {
  396. return err
  397. } else if !has {
  398. return nil
  399. }
  400. sess := x.NewSession()
  401. defer sessionRelease(sess)
  402. if err = sess.Begin(); err != nil {
  403. return err
  404. }
  405. if err = deletePublicKey(sess, id); err != nil {
  406. return err
  407. }
  408. return sess.Commit()
  409. }
  410. // RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
  411. func RewriteAllPublicKeys() error {
  412. sshOpLocker.Lock()
  413. defer sshOpLocker.Unlock()
  414. tmpPath := filepath.Join(SSHPath, "authorized_keys.tmp")
  415. f, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  416. if err != nil {
  417. return err
  418. }
  419. defer os.Remove(tmpPath)
  420. err = x.Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
  421. _, err = f.WriteString((bean.(*PublicKey)).GetAuthorizedString())
  422. return err
  423. })
  424. f.Close()
  425. if err != nil {
  426. return err
  427. }
  428. fpath := filepath.Join(SSHPath, "authorized_keys")
  429. if com.IsExist(fpath) {
  430. if err = os.Remove(fpath); err != nil {
  431. return err
  432. }
  433. }
  434. if err = os.Rename(tmpPath, fpath); err != nil {
  435. return err
  436. }
  437. return nil
  438. }
  439. // ________ .__ ____ __.
  440. // \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__.
  441. // | | \_/ __ \\____ \| | / _ < | || <_/ __ < | |
  442. // | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ |
  443. // /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____|
  444. // \/ \/|__| \/ \/ \/\/
  445. // DeployKey represents deploy key information and its relation with repository.
  446. type DeployKey struct {
  447. ID int64 `xorm:"pk autoincr"`
  448. KeyID int64 `xorm:"UNIQUE(s) INDEX"`
  449. RepoID int64 `xorm:"UNIQUE(s) INDEX"`
  450. Name string
  451. Fingerprint string
  452. Created time.Time `xorm:"CREATED"`
  453. Updated time.Time // Note: Updated must below Created for AfterSet.
  454. HasRecentActivity bool `xorm:"-"`
  455. HasUsed bool `xorm:"-"`
  456. }
  457. func (k *DeployKey) AfterSet(colName string, _ xorm.Cell) {
  458. switch colName {
  459. case "created":
  460. k.HasUsed = k.Updated.After(k.Created)
  461. k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
  462. }
  463. }
  464. func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
  465. // Note: We want error detail, not just true or false here.
  466. has, err := e.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
  467. if err != nil {
  468. return err
  469. } else if has {
  470. return ErrDeployKeyAlreadyExist{keyID, repoID}
  471. }
  472. has, err = e.Where("repo_id=? AND name=?", repoID, name).Get(new(DeployKey))
  473. if err != nil {
  474. return err
  475. } else if has {
  476. return ErrDeployKeyNameAlreadyUsed{repoID, name}
  477. }
  478. return nil
  479. }
  480. // addDeployKey adds new key-repo relation.
  481. func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (err error) {
  482. if err = checkDeployKey(e, keyID, repoID, name); err != nil {
  483. return err
  484. }
  485. _, err = e.Insert(&DeployKey{
  486. KeyID: keyID,
  487. RepoID: repoID,
  488. Name: name,
  489. Fingerprint: fingerprint,
  490. })
  491. return err
  492. }
  493. // HasDeployKey returns true if public key is a deploy key of given repository.
  494. func HasDeployKey(keyID, repoID int64) bool {
  495. has, _ := x.Where("key_id=? AND repo_id=?", keyID, repoID).Get(new(DeployKey))
  496. return has
  497. }
  498. // AddDeployKey add new deploy key to database and authorized_keys file.
  499. func AddDeployKey(repoID int64, name, content string) (err error) {
  500. if err = checkKeyContent(content); err != nil {
  501. return err
  502. }
  503. key := &PublicKey{
  504. Content: content,
  505. Mode: ACCESS_MODE_READ,
  506. Type: KEY_TYPE_DEPLOY,
  507. }
  508. has, err := x.Get(key)
  509. if err != nil {
  510. return err
  511. }
  512. sess := x.NewSession()
  513. defer sessionRelease(sess)
  514. if err = sess.Begin(); err != nil {
  515. return err
  516. }
  517. // First time use this deploy key.
  518. if !has {
  519. if err = addKey(sess, key); err != nil {
  520. return nil
  521. }
  522. }
  523. if err = addDeployKey(sess, key.ID, repoID, name, key.Fingerprint); err != nil {
  524. return err
  525. }
  526. return sess.Commit()
  527. }
  528. // GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
  529. func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
  530. key := &DeployKey{
  531. KeyID: keyID,
  532. RepoID: repoID,
  533. }
  534. _, err := x.Get(key)
  535. return key, err
  536. }
  537. // UpdateDeployKey updates deploy key information.
  538. func UpdateDeployKey(key *DeployKey) error {
  539. _, err := x.Id(key.ID).AllCols().Update(key)
  540. return err
  541. }
  542. // DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
  543. func DeleteDeployKey(id int64) error {
  544. key := &DeployKey{ID: id}
  545. has, err := x.Id(key.ID).Get(key)
  546. if err != nil {
  547. return err
  548. } else if !has {
  549. return nil
  550. }
  551. sess := x.NewSession()
  552. defer sessionRelease(sess)
  553. if err = sess.Begin(); err != nil {
  554. return err
  555. }
  556. if _, err = sess.Id(key.ID).Delete(new(DeployKey)); err != nil {
  557. return fmt.Errorf("delete deploy key[%d]: %v", key.ID, err)
  558. }
  559. // Check if this is the last reference to same key content.
  560. has, err = sess.Where("key_id=?", key.KeyID).Get(new(DeployKey))
  561. if err != nil {
  562. return err
  563. } else if !has {
  564. if err = deletePublicKey(sess, key.KeyID); err != nil {
  565. return err
  566. }
  567. }
  568. return sess.Commit()
  569. }
  570. // ListDeployKeys returns all deploy keys by given repository ID.
  571. func ListDeployKeys(repoID int64) ([]*DeployKey, error) {
  572. keys := make([]*DeployKey, 0, 5)
  573. return keys, x.Where("repo_id=?", repoID).Find(&keys)
  574. }