internal/control/identity.go

a831d1ab09705002f965fb91989e86d1638c0ed8
gitbay/internal/control/identity.go history · blame · raw

133 lines · 3721 bytes

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