internal/control/deploykey.go
149 lines · 4703 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "time"
8
9 "golang.org/x/crypto/ssh"
10
11 "gitbay.org/gitbay/internal/policy"
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16func init() {
17 register(Command{Path: []string{"repo", "deploy-key", "add"},
18 Summary: "bind a read-only (or --rw) key to one repository",
19 Usage: "repo deploy-key add <owner/name> [--rw] [--ttl 30d|720h] < key.pub",
20 Flags: []Flag{
21 {"--rw", "", "the key may push, not just fetch", ""},
22 {"--ttl", "30d|720h", "how long the key authenticates", "never expires"},
23 },
24 Examples: []string{"repo deploy-key add krz/gitbay < key.pub", "repo deploy-key add krz/gitbay --ttl 30d < key.pub"},
25 ReadsStdin: true,
26 MintsCredential: true, NeedsRecentSignIn: true, Run: runDeployKeyAdd})
27 register(Command{Path: []string{"repo", "deploy-key", "list"},
28 Summary: "list deploy keys",
29 Usage: "repo deploy-key list <owner/name>",
30 Examples: []string{"repo deploy-key list krz/gitbay"},
31 ReadOnly: true, Run: runDeployKeyList})
32 register(Command{Path: []string{"repo", "deploy-key", "remove"},
33 Summary: "remove a deploy key",
34 Usage: "repo deploy-key remove <owner/name> <fingerprint>",
35 Examples: []string{"repo deploy-key remove krz/gitbay SHA256:abcd1234"},
36 Run: runDeployKeyRemove})
37}
38
39func runDeployKeyAdd(c *Ctx, args []string) int {
40 f, err := c.parseArgs(args, flagSpec{Values: []string{"--ttl"}, Bools: []string{"--rw"}, MaxPos: 1, Usage: c.Cmd.Usage})
41 if err != nil {
42 return c.fail(protocol.ExitUsage, "%v", err)
43 }
44 path := f.pos(0)
45 if path == "" {
46 return c.usage()
47 }
48 mode := "ro"
49 if f.Has("--rw") {
50 mode = "rw"
51 }
52 expires, code := c.ttlFlag(f)
53 if code >= 0 {
54 return code
55 }
56 repo, code := resolveRepo(c, path, policy.CanAdmin)
57 if code >= 0 {
58 return code
59 }
60 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
61 if err != nil {
62 return c.fail(protocol.ExitFailure, "reading key: %v", err)
63 }
64 pub, comment, _, _, err := ssh.ParseAuthorizedKey(raw)
65 if err != nil {
66 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
67 }
68 label, err := keyLabel(comment)
69 if err != nil {
70 return c.fail(protocol.ExitUsage, "%v", err)
71 }
72 fp := ssh.FingerprintSHA256(pub)
73 scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
74 if err := c.Store.AddSSHKeyFrom(c.User.ID, fp, pub.Type(), pub.Marshal(), scope, label, store.KeyOrigin{CreatedByToken: c.TokenID, ExpiresAt: expires}); err != nil {
75 if errors.Is(err, store.ErrDuplicateKey) {
76 return c.failErr(err)
77 }
78 return c.fail(protocol.ExitFailure, "%v", err)
79 }
80 d := map[string]any{"fingerprint": fp, "mode": mode}
81 if expires != nil {
82 d["expires_at"] = expires
83 }
84 return c.emit(d, func(w io.Writer) {
85 line := fmt.Sprintf("deploy key %s (%s) bound to %s", fp, mode, repo.Path())
86 if expires != nil {
87 line += ", expires " + c.expiresText(expires, time.Now())
88 }
89 fmt.Fprintln(w, line)
90 })
91}
92
93func runDeployKeyList(c *Ctx, args []string) int {
94 if len(args) != 1 {
95 return c.usage()
96 }
97 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
98 if code >= 0 {
99 return code
100 }
101 keys, err := c.Store.ListDeployKeys(repo.ID)
102 if err != nil {
103 return c.fail(protocol.ExitFailure, "%v", err)
104 }
105 type out struct {
106 Fingerprint string `json:"fingerprint"`
107 Algo string `json:"algo"`
108 Mode string `json:"mode"`
109 Label string `json:"label"`
110 LastUsedAt string `json:"last_used_at,omitempty"`
111 ExpiresAt *time.Time `json:"expires_at,omitempty"`
112 }
113 var ds []out
114 for _, k := range keys {
115 mode := "ro"
116 if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
117 mode = "rw"
118 }
119 ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label, k.LastUsedAt, k.ExpiresAt})
120 }
121 now := time.Now()
122 return c.emit(ds, func(w io.Writer) {
123 tb := c.table(w, "FINGERPRINT", "ALGO", "MODE", "LABEL", "USED", "EXPIRES")
124 for _, d := range ds {
125 tb.row(cFlex(d.Fingerprint), cText(d.Algo), cState(d.Mode), cText(d.Label),
126 cText(c.usedText(d.LastUsedAt)), cText(c.expiresText(d.ExpiresAt, now)))
127 }
128 tb.flush()
129 })
130}
131
132func runDeployKeyRemove(c *Ctx, args []string) int {
133 if len(args) != 2 {
134 return c.usage()
135 }
136 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
137 if code >= 0 {
138 return code
139 }
140 if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
141 if errors.Is(err, store.ErrNotFound) {
142 return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
143 }
144 return c.fail(protocol.ExitFailure, "%v", err)
145 }
146 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
147 fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
148 })
149}