internal/control/deploykey.go
119 lines · 3616 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.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub")
38 }
39 path = a
40 }
41 }
42 if path == "" {
43 return c.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub")
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, _, _, _, 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 fp := ssh.FingerprintSHA256(pub)
58 scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
59 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil {
60 if errors.Is(err, store.ErrDuplicateKey) {
61 return c.fail(protocol.ExitUsage, "%v", err)
62 }
63 return c.fail(protocol.ExitFailure, "%v", err)
64 }
65 return c.emit(map[string]string{"fingerprint": fp, "mode": mode}, func(w io.Writer) {
66 fmt.Fprintf(w, "deploy key %s (%s) bound to %s\n", fp, mode, repo.Path())
67 })
68}
69
70func runDeployKeyList(c *Ctx, args []string) int {
71 if len(args) != 1 {
72 return c.fail(protocol.ExitUsage, "usage: repo deploy-key list <owner/name>")
73 }
74 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
75 if code >= 0 {
76 return code
77 }
78 keys, err := c.Store.ListDeployKeys(repo.ID)
79 if err != nil {
80 return c.fail(protocol.ExitFailure, "%v", err)
81 }
82 type out struct {
83 Fingerprint string `json:"fingerprint"`
84 Algo string `json:"algo"`
85 Mode string `json:"mode"`
86 }
87 var ds []out
88 for _, k := range keys {
89 mode := "ro"
90 if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
91 mode = "rw"
92 }
93 ds = append(ds, out{k.Fingerprint, k.Algo, mode})
94 }
95 return c.emit(ds, func(w io.Writer) {
96 for _, d := range ds {
97 fmt.Fprintf(w, "%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Mode)
98 }
99 })
100}
101
102func runDeployKeyRemove(c *Ctx, args []string) int {
103 if len(args) != 2 {
104 return c.fail(protocol.ExitUsage, "usage: repo deploy-key remove <owner/name> <fingerprint>")
105 }
106 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
107 if code >= 0 {
108 return code
109 }
110 if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
111 if errors.Is(err, store.ErrNotFound) {
112 return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
113 }
114 return c.fail(protocol.ExitFailure, "%v", err)
115 }
116 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
117 fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
118 })
119}