internal/control/deploykey.go

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

149 lines · 4674 bytes

  1package control
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"time"
  8
  9	"golang.org/x/crypto/ssh"
 10
 11	"gitbay.org/gitbay/internal/policy"
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16func init() {
 17	register(Command{Path: []string{"repo", "deploy-key", "add"},
 18		Summary: "bind a read-only (or --rw) key to one repository",
 19		Usage:   "repo deploy-key add <owner/name> [--rw] [--ttl 30d|720h] < key.pub",
 20		Flags: []Flag{
 21			{"--rw", "", "the key may push, not just fetch", ""},
 22			{"--ttl", "30d|720h", "how long the key authenticates", "never expires"},
 23		},
 24		Examples:        []string{"repo deploy-key add krz/gitbay < key.pub", "repo deploy-key add krz/gitbay --ttl 30d < key.pub"},
 25		ReadsStdin:      true,
 26		MintsCredential: true, Run: runDeployKeyAdd})
 27	register(Command{Path: []string{"repo", "deploy-key", "list"},
 28		Summary:  "list deploy keys",
 29		Usage:    "repo deploy-key list <owner/name>",
 30		Examples: []string{"repo deploy-key list krz/gitbay"},
 31		ReadOnly: true, Run: runDeployKeyList})
 32	register(Command{Path: []string{"repo", "deploy-key", "remove"},
 33		Summary:  "remove a deploy key",
 34		Usage:    "repo deploy-key remove <owner/name> <fingerprint>",
 35		Examples: []string{"repo deploy-key remove krz/gitbay SHA256:abcd1234"},
 36		Run:      runDeployKeyRemove})
 37}
 38
 39func runDeployKeyAdd(c *Ctx, args []string) int {
 40	f, err := c.parseArgs(args, flagSpec{Values: []string{"--ttl"}, Bools: []string{"--rw"}, MaxPos: 1, Usage: c.Cmd.Usage})
 41	if err != nil {
 42		return c.fail(protocol.ExitUsage, "%v", err)
 43	}
 44	path := f.pos(0)
 45	if path == "" {
 46		return c.usage()
 47	}
 48	mode := "ro"
 49	if f.Has("--rw") {
 50		mode = "rw"
 51	}
 52	expires, code := c.ttlFlag(f)
 53	if code >= 0 {
 54		return code
 55	}
 56	repo, code := resolveRepo(c, path, policy.CanAdmin)
 57	if code >= 0 {
 58		return code
 59	}
 60	raw, err := io.ReadAll(io.LimitReader(c.Stdin, 64<<10))
 61	if err != nil {
 62		return c.fail(protocol.ExitFailure, "reading key: %v", err)
 63	}
 64	pub, comment, _, _, err := ssh.ParseAuthorizedKey(raw)
 65	if err != nil {
 66		return c.fail(protocol.ExitUsage, "not a valid public key in authorized_keys format: %v", err)
 67	}
 68	label, err := keyLabel(comment)
 69	if err != nil {
 70		return c.fail(protocol.ExitUsage, "%v", err)
 71	}
 72	fp := ssh.FingerprintSHA256(pub)
 73	scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode)
 74	if err := c.Store.AddSSHKeyFrom(c.User.ID, fp, pub.Type(), pub.Marshal(), scope, label, store.KeyOrigin{CreatedByToken: c.TokenID, ExpiresAt: expires}); err != nil {
 75		if errors.Is(err, store.ErrDuplicateKey) {
 76			return c.failErr(err)
 77		}
 78		return c.fail(protocol.ExitFailure, "%v", err)
 79	}
 80	d := map[string]any{"fingerprint": fp, "mode": mode}
 81	if expires != nil {
 82		d["expires_at"] = expires
 83	}
 84	return c.emit(d, func(w io.Writer) {
 85		line := fmt.Sprintf("deploy key %s (%s) bound to %s", fp, mode, repo.Path())
 86		if expires != nil {
 87			line += ", expires " + expiresText(expires, time.Now())
 88		}
 89		fmt.Fprintln(w, line)
 90	})
 91}
 92
 93func runDeployKeyList(c *Ctx, args []string) int {
 94	if len(args) != 1 {
 95		return c.usage()
 96	}
 97	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
 98	if code >= 0 {
 99		return code
100	}
101	keys, err := c.Store.ListDeployKeys(repo.ID)
102	if err != nil {
103		return c.fail(protocol.ExitFailure, "%v", err)
104	}
105	type out struct {
106		Fingerprint string     `json:"fingerprint"`
107		Algo        string     `json:"algo"`
108		Mode        string     `json:"mode"`
109		Label       string     `json:"label"`
110		LastUsedAt  string     `json:"last_used_at,omitempty"`
111		ExpiresAt   *time.Time `json:"expires_at,omitempty"`
112	}
113	var ds []out
114	for _, k := range keys {
115		mode := "ro"
116		if policy.DeployScopeAllows(k.Scope, repo.ID, true) {
117			mode = "rw"
118		}
119		ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label, k.LastUsedAt, k.ExpiresAt})
120	}
121	now := time.Now()
122	return c.emit(ds, func(w io.Writer) {
123		tb := c.table(w, "FINGERPRINT", "ALGO", "MODE", "LABEL", "USED", "EXPIRES")
124		for _, d := range ds {
125			tb.row(cFlex(d.Fingerprint), cText(d.Algo), cState(d.Mode), cText(d.Label),
126				cText(c.usedText(d.LastUsedAt)), cText(expiresText(d.ExpiresAt, now)))
127		}
128		tb.flush()
129	})
130}
131
132func runDeployKeyRemove(c *Ctx, args []string) int {
133	if len(args) != 2 {
134		return c.usage()
135	}
136	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
137	if code >= 0 {
138		return code
139	}
140	if err := c.Store.RemoveDeployKey(repo.ID, args[1]); err != nil {
141		if errors.Is(err, store.ErrNotFound) {
142			return c.fail(protocol.ExitNotFound, "no deploy key %s on %s", args[1], repo.Path())
143		}
144		return c.fail(protocol.ExitFailure, "%v", err)
145	}
146	return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
147		fmt.Fprintf(w, "removed deploy key %s from %s\n", args[1], repo.Path())
148	})
149}