internal/control/identity.go
139 lines · 3900 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 scope := "full"
85 for i := 0; i < len(args); i++ {
86 switch args[i] {
87 case "--scope":
88 if i+1 >= len(args) {
89 return c.fail(protocol.ExitUsage, "--scope requires a value")
90 }
91 scope = args[i+1]
92 i++
93 default:
94 return c.fail(protocol.ExitUsage, "usage: keys add [--scope full|git|runner] < key.pub")
95 }
96 }
97 if scope != "full" && scope != "git" && scope != "runner" {
98 // deploy:* scopes are granted via repo settings, not self-service.
99 return c.fail(protocol.ExitUsage, "scope must be full, git or runner")
100 }
101 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
102 if err != nil {
103 return c.fail(protocol.ExitFailure, "reading key: %v", err)
104 }
105 pub, _, _, _, err := ssh.ParseAuthorizedKey(raw)
106 if err != nil {
107 return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
108 }
109 fp := ssh.FingerprintSHA256(pub)
110 if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil {
111 if errors.Is(err, store.ErrDuplicateKey) {
112 return c.fail(protocol.ExitUsage, "%v", err)
113 }
114 return c.fail(protocol.ExitFailure, "adding key: %v", err)
115 }
116 type out struct {
117 Fingerprint string `json:"fingerprint"`
118 Scope string `json:"scope"`
119 }
120 d := out{fp, scope}
121 return c.emit(d, func(w io.Writer) {
122 fmt.Fprintf(w, "added %s (%s)\n", d.Fingerprint, d.Scope)
123 })
124}
125
126func runKeysRemove(c *Ctx, args []string) int {
127 if len(args) != 1 {
128 return c.fail(protocol.ExitUsage, "usage: keys remove <fingerprint>")
129 }
130 if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil {
131 if errors.Is(err, store.ErrNotFound) {
132 return c.fail(protocol.ExitNotFound, "no key with fingerprint %s on your account", args[0])
133 }
134 return c.fail(protocol.ExitFailure, "removing key: %v", err)
135 }
136 return c.emit(map[string]string{"removed": args[0]}, func(w io.Writer) {
137 fmt.Fprintf(w, "removed %s\n", args[0])
138 })
139}