internal/control/deploykey.go

a32f7f2001c1c0bf38ba8c3360e6bc7ca3982fee
gitbay/internal/control/deploykey.go history · blame · raw

124 lines · 3472 bytes

  1package control
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7
  8	"golang.org/x/crypto/ssh"
  9
 10	"gitbay.org/gitbay/internal/policy"
 11	"gitbay.org/gitbay/internal/protocol"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15func init() {
 16	register(Command{Path: []string{"repo", "deploy-key", "add"},
 17		Summary:    "bind a read-only (or --rw) key to one repository",
 18		Usage:      "repo deploy-key add <owner/name> [--rw] < key.pub",
 19		ReadsStdin: true, Run: runDeployKeyAdd})
 20	register(Command{Path: []string{"repo", "deploy-key", "list"},
 21		Summary: "list deploy keys",
 22		Usage:   "repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList})
 23	register(Command{Path: []string{"repo", "deploy-key", "remove"},
 24		Summary: "remove a deploy key",
 25		Usage:   "repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove})
 26}
 27
 28func runDeployKeyAdd(c *Ctx, args []string) int {
 29	mode := "ro"
 30	var path string
 31	for _, a := range args {
 32		switch a {
 33		case "--rw":
 34			mode = "rw"
 35		default:
 36			if path != "" {
 37				return c.usage()
 38			}
 39			path = a
 40		}
 41	}
 42	if path == "" {
 43		return c.usage()
 44	}
 45	repo, code := resolveRepo(c, path, policy.CanAdmin)
 46	if code >= 0 {
 47		return code
 48	}
 49	raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
 50	if err != nil {
 51		return c.fail(protocol.ExitFailure, "reading key: %v", err)
 52	}
 53	pub, comment, _, _, err := ssh.ParseAuthorizedKey(raw)
 54	if err != nil {
 55		return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
 56	}
 57	label, err := keyLabel(comment)
 58	if err != nil {
 59		return c.fail(protocol.ExitUsage, "%v", err)
 60	}
 61	fp := ssh.FingerprintSHA256(pub)
 62	scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
 63	if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope, label); err != nil {
 64		if errors.Is(err, store.ErrDuplicateKey) {
 65			return c.failErr(err)
 66		}
 67		return c.fail(protocol.ExitFailure, "%v", err)
 68	}
 69	return c.emit(map[string]string{"fingerprint": fp, "mode": mode}, func(w io.Writer) {
 70		fmt.Fprintf(w, "deploy key %s (%s) bound to %s\n", fp, mode, repo.Path())
 71	})
 72}
 73
 74func runDeployKeyList(c *Ctx, args []string) int {
 75	if len(args) != 1 {
 76		return c.usage()
 77	}
 78	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
 79	if code >= 0 {
 80		return code
 81	}
 82	keys, err := c.Store.ListDeployKeys(repo.ID)
 83	if err != nil {
 84		return c.fail(protocol.ExitFailure, "%v", err)
 85	}
 86	type out struct {
 87		Fingerprint string `json:"fingerprint"`
 88		Algo        string `json:"algo"`
 89		Mode        string `json:"mode"`
 90		Label       string `json:"label"`
 91	}
 92	var ds []out
 93	for _, k := range keys {
 94		mode := "ro"
 95		if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
 96			mode = "rw"
 97		}
 98		ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label})
 99	}
100	return c.emit(ds, func(w io.Writer) {
101		for _, d := range ds {
102			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", d.Fingerprint, d.Algo, d.Mode, d.Label)
103		}
104	})
105}
106
107func runDeployKeyRemove(c *Ctx, args []string) int {
108	if len(args) != 2 {
109		return c.usage()
110	}
111	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
112	if code >= 0 {
113		return code
114	}
115	if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
116		if errors.Is(err, store.ErrNotFound) {
117			return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
118		}
119		return c.fail(protocol.ExitFailure, "%v", err)
120	}
121	return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
122		fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
123	})
124}