internal/control/deploykey.go
116 lines · 3574 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7
8 "golang.org/x/crypto/ssh"
9
10 "gitbay.org/gitbay/internal/policy"
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func init() {
16 register(Command{Path: []string{"repo", "deploy-key", "add"},
17 Summary: "bind a read-only (or --rw) key to one repository: repo deploy-key add <owner/name> [--rw] < key.pub",
18 ReadsStdin: true, Run: runDeployKeyAdd})
19 register(Command{Path: []string{"repo", "deploy-key", "list"},
20 Summary: "list deploy keys: repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList})
21 register(Command{Path: []string{"repo", "deploy-key", "remove"},
22 Summary: "remove a deploy key: repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove})
23}
24
25func runDeployKeyAdd(c *Ctx, args []string) int {
26 mode := "ro"
27 var path string
28 for _, a := range args {
29 switch a {
30 case "--rw":
31 mode = "rw"
32 default:
33 if path != "" {
34 return c.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub")
35 }
36 path = a
37 }
38 }
39 if path == "" {
40 return c.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub")
41 }
42 repo, code := resolveRepo(c, path, policy.CanAdmin)
43 if code >= 0 {
44 return code
45 }
46 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
47 if err != nil {
48 return c.fail(protocol.ExitFailure, "reading key: %v", err)
49 }
50 pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
51 if err != nil {
52 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
53 }
54 fp := ssh.FingerprintSHA256(pub)
55 scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
56 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil {
57 if errors.Is(err, store.ErrDuplicateKey) {
58 return c.fail(protocol.ExitUsage, "%v", err)
59 }
60 return c.fail(protocol.ExitFailure, "%v", err)
61 }
62 return c.emit(map[string]string{"fingerprint": fp, "mode": mode}, func(w io.Writer) {
63 fmt.Fprintf(w, "deploy key %s (%s) bound to %s\n", fp, mode, repo.Path())
64 })
65}
66
67func runDeployKeyList(c *Ctx, args []string) int {
68 if len(args) != 1 {
69 return c.fail(protocol.ExitUsage, "usage: repo deploy-key list <owner/name>")
70 }
71 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
72 if code >= 0 {
73 return code
74 }
75 keys, err := c.Store.ListDeployKeys(repo.ID)
76 if err != nil {
77 return c.fail(protocol.ExitFailure, "%v", err)
78 }
79 type out struct {
80 Fingerprint string `json:"fingerprint"`
81 Algo string `json:"algo"`
82 Mode string `json:"mode"`
83 }
84 var ds []out
85 for _, k := range keys {
86 mode := "ro"
87 if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
88 mode = "rw"
89 }
90 ds = append(ds, out{k.Fingerprint, k.Algo, mode})
91 }
92 return c.emit(ds, func(w io.Writer) {
93 for _, d := range ds {
94 fmt.Fprintf(w, "%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Mode)
95 }
96 })
97}
98
99func runDeployKeyRemove(c *Ctx, args []string) int {
100 if len(args) != 2 {
101 return c.fail(protocol.ExitUsage, "usage: repo deploy-key remove <owner/name> <fingerprint>")
102 }
103 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
104 if code >= 0 {
105 return code
106 }
107 if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
108 if errors.Is(err, store.ErrNotFound) {
109 return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
110 }
111 return c.fail(protocol.ExitFailure, "%v", err)
112 }
113 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
114 fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
115 })
116}