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}