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