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