internal/control/deploykey.go

v0.3.0
gitbay/internal/control/deploykey.go history · blame · raw

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