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