internal/control/identity.go
203 lines · 5948 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strings"
8 "unicode"
9
10 "golang.org/x/crypto/ssh"
11
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16func init() {
17 register(Command{
18 Path: []string{"whoami"},
19 Summary: "show the authenticated account",
20 Usage: "whoami",
21 ReadOnly: true,
22 Run: runWhoami,
23 })
24 register(Command{
25 Path: []string{"keys", "list"},
26 Summary: "list registered SSH keys",
27 Usage: "keys list",
28 ReadOnly: true,
29 Run: runKeysList,
30 })
31 register(Command{
32 Path: []string{"keys", "add"},
33 Summary: "register an SSH public key (authorized_keys format)",
34 Usage: "keys add [--scope full|git|runner] [--label <text>] < key.pub",
35 ReadsStdin: true,
36 Run: runKeysAdd,
37 })
38 register(Command{
39 Path: []string{"keys", "label"},
40 Summary: "name a key; an empty label clears it",
41 Usage: "keys label <fingerprint> [<text>]",
42 Run: runKeysLabel,
43 })
44 register(Command{
45 Path: []string{"keys", "remove"},
46 Summary: "remove an SSH key by fingerprint",
47 Usage: "keys remove <fingerprint>",
48 Run: runKeysRemove,
49 })
50}
51
52func runWhoami(c *Ctx, args []string) int {
53 if len(args) != 0 {
54 return c.fail(protocol.ExitUsage, "usage: whoami [--json]")
55 }
56 type out struct {
57 Username string `json:"username"`
58 Admin bool `json:"admin"`
59 KeyScope string `json:"key_scope"`
60 }
61 d := out{Username: c.User.Username, Admin: c.User.IsAdmin, KeyScope: c.Scope}
62 return c.emit(d, func(w io.Writer) {
63 fmt.Fprintln(w, d.Username)
64 })
65}
66
67func runKeysList(c *Ctx, args []string) int {
68 if len(args) != 0 {
69 return c.fail(protocol.ExitUsage, "usage: keys list [--json]")
70 }
71 keys, err := c.Store.ListSSHKeys(c.User.ID)
72 if err != nil {
73 return c.fail(protocol.ExitFailure, "listing keys: %v", err)
74 }
75 type out struct {
76 Fingerprint string `json:"fingerprint"`
77 Algo string `json:"algo"`
78 Scope string `json:"scope"`
79 Label string `json:"label"`
80 }
81 var ds []out
82 for _, k := range keys {
83 ds = append(ds, out{k.Fingerprint, k.Algo, k.Scope, k.Label})
84 }
85 return c.emit(ds, func(w io.Writer) {
86 for _, d := range ds {
87 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Scope, d.Label)
88 }
89 })
90}
91
92// maxKeyLabel bounds a key's name. Labels are display text, one line.
93const maxKeyLabel = 64
94
95// keyLabel normalises a label: surrounding space trimmed, control
96// characters refused, length capped. An empty result is a valid "no
97// label".
98func keyLabel(s string) (string, error) {
99 s = strings.TrimSpace(s)
100 if len(s) > maxKeyLabel {
101 return "", fmt.Errorf("label is longer than %d bytes", maxKeyLabel)
102 }
103 for _, r := range s {
104 if unicode.IsControl(r) {
105 return "", errors.New("label must be a single line of printable text")
106 }
107 }
108 return s, nil
109}
110
111func runKeysAdd(c *Ctx, args []string) int {
112 f, err := parseFlags(args, flagSpec{Values: []string{"--scope", "--label"}, MaxPos: 0, Usage: "keys add [--scope full|git|runner] [--label <text>] < key.pub"})
113 if err != nil {
114 return c.fail(protocol.ExitUsage, "%v", err)
115 }
116 scope := "full"
117 if f.Has("--scope") {
118 scope = f.Value("--scope")
119 }
120 if scope != "full" && scope != "git" && scope != "runner" {
121 // deploy:* scopes are granted via repo settings, not self-service.
122 return c.fail(protocol.ExitUsage, "scope must be full, git or runner")
123 }
124 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
125 if err != nil {
126 return c.fail(protocol.ExitFailure, "reading key: %v", err)
127 }
128 pub, comment, _, _, err := ssh.ParseAuthorizedKey(raw)
129 if err != nil {
130 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
131 }
132 // The key's own comment is the label unless --label says otherwise.
133 label := comment
134 if f.Has("--label") {
135 label = f.Value("--label")
136 }
137 if label, err = keyLabel(label); err != nil {
138 return c.fail(protocol.ExitUsage, "%v", err)
139 }
140 fp := ssh.FingerprintSHA256(pub)
141 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope, label); err != nil {
142 if errors.Is(err, store.ErrDuplicateKey) {
143 return c.failErr(err)
144 }
145 return c.fail(protocol.ExitFailure, "adding key: %v", err)
146 }
147 type out struct {
148 Fingerprint string `json:"fingerprint"`
149 Scope string `json:"scope"`
150 Label string `json:"label"`
151 }
152 d := out{fp, scope, label}
153 return c.emit(d, func(w io.Writer) {
154 if d.Label != "" {
155 fmt.Fprintf(w, "added %s (%s) %s\n", d.Fingerprint, d.Scope, d.Label)
156 return
157 }
158 fmt.Fprintf(w, "added %s (%s)\n", d.Fingerprint, d.Scope)
159 })
160}
161
162func runKeysLabel(c *Ctx, args []string) int {
163 if len(args) < 1 || len(args) > 2 {
164 return c.fail(protocol.ExitUsage, "usage: keys label <fingerprint> [<text>]")
165 }
166 label := ""
167 if len(args) == 2 {
168 label = args[1]
169 }
170 label, err := keyLabel(label)
171 if err != nil {
172 return c.fail(protocol.ExitUsage, "%v", err)
173 }
174 if err := c.Store.SetSSHKeyLabel(c.User.ID, args[0], label); err != nil {
175 if errors.Is(err, store.ErrNotFound) {
176 return c.fail(protocol.ExitNotFound, "no key with fingerprint %s on your account", args[0])
177 }
178 return c.fail(protocol.ExitFailure, "labelling key: %v", err)
179 }
180 d := map[string]string{"fingerprint": args[0], "label": label}
181 return c.emit(d, func(w io.Writer) {
182 if label == "" {
183 fmt.Fprintf(w, "cleared label on %s\n", args[0])
184 return
185 }
186 fmt.Fprintf(w, "%s is now %q\n", args[0], label)
187 })
188}
189
190func runKeysRemove(c *Ctx, args []string) int {
191 if len(args) != 1 {
192 return c.fail(protocol.ExitUsage, "usage: keys remove <fingerprint>")
193 }
194 if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil {
195 if errors.Is(err, store.ErrNotFound) {
196 return c.fail(protocol.ExitNotFound, "no key with fingerprint %s on your account", args[0])
197 }
198 return c.fail(protocol.ExitFailure, "removing key: %v", err)
199 }
200 return c.emit(map[string]string{"removed": args[0]}, func(w io.Writer) {
201 fmt.Fprintf(w, "removed %s\n", args[0])
202 })
203}