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