internal/control/identity.go

353f68a2e57ac692964b73ae1f9acd91fcdcae20
gitbay/internal/control/identity.go history · blame · raw

203 lines · 5745 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.usage()
 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.usage()
 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.usage()
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.usage()
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}