krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: 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}