internal/control/identity.go

4bfcb3893d5ad29d5c6ba0725fd2291d20003e30
gitbay/internal/control/identity.go history · blame · raw

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}