internal/control/identity.go
134 lines · 3827 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7
8 "golang.org/x/crypto/ssh"
9
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14func init() {
15 register(Command{
16 Path: []string{"whoami"},
17 Summary: "show the authenticated account",
18 Usage: "whoami",
19 ReadOnly: true,
20 Run: runWhoami,
21 })
22 register(Command{
23 Path: []string{"keys", "list"},
24 Summary: "list registered SSH keys",
25 Usage: "keys list",
26 ReadOnly: true,
27 Run: runKeysList,
28 })
29 register(Command{
30 Path: []string{"keys", "add"},
31 Summary: "register an SSH public key (authorized_keys format)",
32 Usage: "keys add [--scope full|git|runner] < key.pub",
33 ReadsStdin: true,
34 Run: runKeysAdd,
35 })
36 register(Command{
37 Path: []string{"keys", "remove"},
38 Summary: "remove an SSH key by fingerprint",
39 Usage: "keys remove <fingerprint>",
40 Run: runKeysRemove,
41 })
42}
43
44func runWhoami(c *Ctx, args []string) int {
45 if len(args) != 0 {
46 return c.fail(protocol.ExitUsage, "usage: whoami [--json]")
47 }
48 type out struct {
49 Username string `json:"username"`
50 Admin bool `json:"admin"`
51 KeyScope string `json:"key_scope"`
52 }
53 d := out{Username: c.User.Username, Admin: c.User.IsAdmin, KeyScope: c.Scope}
54 return c.emit(d, func(w io.Writer) {
55 fmt.Fprintln(w, d.Username)
56 })
57}
58
59func runKeysList(c *Ctx, args []string) int {
60 if len(args) != 0 {
61 return c.fail(protocol.ExitUsage, "usage: keys list [--json]")
62 }
63 keys, err := c.Store.ListSSHKeys(c.User.ID)
64 if err != nil {
65 return c.fail(protocol.ExitFailure, "listing keys: %v", err)
66 }
67 type out struct {
68 Fingerprint string `json:"fingerprint"`
69 Algo string `json:"algo"`
70 Scope string `json:"scope"`
71 }
72 var ds []out
73 for _, k := range keys {
74 ds = append(ds, out{k.Fingerprint, k.Algo, k.Scope})
75 }
76 return c.emit(ds, func(w io.Writer) {
77 for _, d := range ds {
78 fmt.Fprintf(w, "%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Scope)
79 }
80 })
81}
82
83func runKeysAdd(c *Ctx, args []string) int {
84 f, err := parseFlags(args, flagSpec{Values: []string{"--scope"}, MaxPos: 0, Usage: "keys add [--scope full|git|runner] < key.pub"})
85 if err != nil {
86 return c.fail(protocol.ExitUsage, "%v", err)
87 }
88 scope := "full"
89 if f.Has("--scope") {
90 scope = f.Value("--scope")
91 }
92 if scope != "full" && scope != "git" && scope != "runner" {
93 // deploy:* scopes are granted via repo settings, not self-service.
94 return c.fail(protocol.ExitUsage, "scope must be full, git or runner")
95 }
96 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
97 if err != nil {
98 return c.fail(protocol.ExitFailure, "reading key: %v", err)
99 }
100 pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
101 if err != nil {
102 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
103 }
104 fp := ssh.FingerprintSHA256(pub)
105 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil {
106 if errors.Is(err, store.ErrDuplicateKey) {
107 return c.failErr(err)
108 }
109 return c.fail(protocol.ExitFailure, "adding key: %v", err)
110 }
111 type out struct {
112 Fingerprint string `json:"fingerprint"`
113 Scope string `json:"scope"`
114 }
115 d := out{fp, scope}
116 return c.emit(d, func(w io.Writer) {
117 fmt.Fprintf(w, "added %s (%s)\n", d.Fingerprint, d.Scope)
118 })
119}
120
121func runKeysRemove(c *Ctx, args []string) int {
122 if len(args) != 1 {
123 return c.fail(protocol.ExitUsage, "usage: keys remove <fingerprint>")
124 }
125 if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil {
126 if errors.Is(err, store.ErrNotFound) {
127 return c.fail(protocol.ExitNotFound, "no key with fingerprint %s on your account", args[0])
128 }
129 return c.fail(protocol.ExitFailure, "removing key: %v", err)
130 }
131 return c.emit(map[string]string{"removed": args[0]}, func(w io.Writer) {
132 fmt.Fprintf(w, "removed %s\n", args[0])
133 })
134}