internal/control/deploykey.go
134 lines · 3831 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",
18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub",
19 Flags: []Flag{
20 {"--rw", "", "the key may push, not just fetch", ""},
21 },
22 Examples: []string{"repo deploy-key add krz/gitbay < key.pub"},
23 ReadsStdin: true, Run: runDeployKeyAdd})
24 register(Command{Path: []string{"repo", "deploy-key", "list"},
25 Summary: "list deploy keys",
26 Usage: "repo deploy-key list <owner/name>",
27 Examples: []string{"repo deploy-key list krz/gitbay"},
28 ReadOnly: true, Run: runDeployKeyList})
29 register(Command{Path: []string{"repo", "deploy-key", "remove"},
30 Summary: "remove a deploy key",
31 Usage: "repo deploy-key remove <owner/name> <fingerprint>",
32 Examples: []string{"repo deploy-key remove krz/gitbay SHA256:abcd1234"},
33 Run: runDeployKeyRemove})
34}
35
36func runDeployKeyAdd(c *Ctx, args []string) int {
37 mode := "ro"
38 var path string
39 for _, a := range args {
40 switch a {
41 case "--rw":
42 mode = "rw"
43 default:
44 if path != "" {
45 return c.usage()
46 }
47 path = a
48 }
49 }
50 if path == "" {
51 return c.usage()
52 }
53 repo, code := resolveRepo(c, path, policy.CanAdmin)
54 if code >= 0 {
55 return code
56 }
57 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
58 if err != nil {
59 return c.fail(protocol.ExitFailure, "reading key: %v", err)
60 }
61 pub, comment, _, _, err := ssh.ParseAuthorizedKey(raw)
62 if err != nil {
63 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
64 }
65 label, err := keyLabel(comment)
66 if err != nil {
67 return c.fail(protocol.ExitUsage, "%v", err)
68 }
69 fp := ssh.FingerprintSHA256(pub)
70 scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
71 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope, label); err != nil {
72 if errors.Is(err, store.ErrDuplicateKey) {
73 return c.failErr(err)
74 }
75 return c.fail(protocol.ExitFailure, "%v", err)
76 }
77 return c.emit(map[string]string{"fingerprint": fp, "mode": mode}, func(w io.Writer) {
78 fmt.Fprintf(w, "deploy key %s (%s) bound to %s\n", fp, mode, repo.Path())
79 })
80}
81
82func runDeployKeyList(c *Ctx, args []string) int {
83 if len(args) != 1 {
84 return c.usage()
85 }
86 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
87 if code >= 0 {
88 return code
89 }
90 keys, err := c.Store.ListDeployKeys(repo.ID)
91 if err != nil {
92 return c.fail(protocol.ExitFailure, "%v", err)
93 }
94 type out struct {
95 Fingerprint string `json:"fingerprint"`
96 Algo string `json:"algo"`
97 Mode string `json:"mode"`
98 Label string `json:"label"`
99 }
100 var ds []out
101 for _, k := range keys {
102 mode := "ro"
103 if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
104 mode = "rw"
105 }
106 ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label})
107 }
108 return c.emit(ds, func(w io.Writer) {
109 tb := c.table(w, "FINGERPRINT", "ALGO", "MODE", "LABEL")
110 for _, d := range ds {
111 tb.row(cFlex(d.Fingerprint), cText(d.Algo), cState(d.Mode), cText(d.Label))
112 }
113 tb.flush()
114 })
115}
116
117func runDeployKeyRemove(c *Ctx, args []string) int {
118 if len(args) != 2 {
119 return c.usage()
120 }
121 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
122 if code >= 0 {
123 return code
124 }
125 if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
126 if errors.Is(err, store.ErrNotFound) {
127 return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
128 }
129 return c.fail(protocol.ExitFailure, "%v", err)
130 }
131 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
132 fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
133 })
134}