internal/control/token.go

d775997ead04022093817ab513287b5886065247
gitbay/internal/control/token.go history · blame · raw

122 lines · 3669 bytes

  1package control
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"strconv"
  8	"strings"
  9	"time"
 10
 11	"gitbay.org/gitbay/internal/protocol"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15func init() {
 16	register(Command{Path: []string{"token", "create"},
 17		Summary: "mint an API token (shown once)",
 18		Usage:   "token create --name <n> [--scope full|read] [--ttl 30d|720h]",
 19		SSHOnly: true, Run: runTokenCreate})
 20	register(Command{Path: []string{"token", "list"},
 21		Summary: "list API tokens",
 22		Usage:   "token list", ReadOnly: true, SSHOnly: true, Run: runTokenList})
 23	register(Command{Path: []string{"token", "revoke"},
 24		Summary: "revoke an API token by name",
 25		Usage:   "token revoke <name>",
 26		SSHOnly: true, Run: runTokenRevoke})
 27}
 28
 29// parseTTL accepts Go durations plus a day suffix ("30d").
 30func parseTTL(s string) (time.Duration, error) {
 31	if days, ok := strings.CutSuffix(s, "d"); ok {
 32		n, err := strconv.Atoi(days)
 33		if err != nil || n < 1 {
 34			return 0, fmt.Errorf("bad ttl %q", s)
 35		}
 36		return time.Duration(n) * 24 * time.Hour, nil
 37	}
 38	return time.ParseDuration(s)
 39}
 40
 41func runTokenCreate(c *Ctx, args []string) int {
 42	f, err := parseFlags(args, flagSpec{Values: []string{"--name", "--scope", "--ttl"}, MaxPos: 0, Usage: "token create --name <n> [--scope full|read] [--ttl 30d]"})
 43	if err != nil {
 44		return c.fail(protocol.ExitUsage, "%v", err)
 45	}
 46	name, scope, ttl := f.Value("--name"), "full", f.Value("--ttl")
 47	if f.Has("--scope") {
 48		scope = f.Value("--scope")
 49	}
 50	if name == "" || (scope != "full" && scope != "read") {
 51		return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]")
 52	}
 53	var expires *time.Time
 54	if ttl != "" {
 55		d, err := parseTTL(ttl)
 56		if err != nil {
 57			return c.failErr(err)
 58		}
 59		t := time.Now().Add(d)
 60		expires = &t
 61	}
 62	raw, _, err := store.NewToken()
 63	if err != nil {
 64		return c.fail(protocol.ExitFailure, "%v", err)
 65	}
 66	// The gb_ prefix makes leaked tokens findable by secret scanners.
 67	token := "gb_" + raw
 68	if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires); err != nil {
 69		return c.failErr(err)
 70	}
 71	type out struct {
 72		Name  string `json:"name"`
 73		Scope string `json:"scope"`
 74		Token string `json:"token"`
 75	}
 76	d := out{name, scope, token}
 77	return c.emit(d, func(w io.Writer) {
 78		fmt.Fprintf(w, "token %q (%s) — shown once, store it now:\n%s\n", d.Name, d.Scope, d.Token)
 79	})
 80}
 81
 82func runTokenList(c *Ctx, args []string) int {
 83	tokens, err := c.Store.ListAPITokens(c.User.ID)
 84	if err != nil {
 85		return c.fail(protocol.ExitFailure, "%v", err)
 86	}
 87	type out struct {
 88		Name       string     `json:"name"`
 89		Scope      string     `json:"scope"`
 90		CreatedAt  string     `json:"created_at"`
 91		ExpiresAt  *time.Time `json:"expires_at,omitempty"`
 92		LastUsedAt *time.Time `json:"last_used_at,omitempty"`
 93	}
 94	var ds []out
 95	for _, t := range tokens {
 96		ds = append(ds, out{t.Name, t.Scope, t.CreatedAt, t.ExpiresAt, t.LastUsedAt})
 97	}
 98	return c.emit(ds, func(w io.Writer) {
 99		for _, d := range ds {
100			exp := "never expires"
101			if d.ExpiresAt != nil {
102				exp = "expires " + d.ExpiresAt.UTC().Format(time.RFC3339)
103			}
104			fmt.Fprintf(w, "%s\t%s\t%s\n", d.Name, d.Scope, exp)
105		}
106	})
107}
108
109func runTokenRevoke(c *Ctx, args []string) int {
110	if len(args) != 1 {
111		return c.fail(protocol.ExitUsage, "usage: token revoke <name>")
112	}
113	if err := c.Store.RevokeAPIToken(c.User.ID, args[0]); err != nil {
114		if errors.Is(err, store.ErrNotFound) {
115			return c.fail(protocol.ExitNotFound, "no token named %q", args[0])
116		}
117		return c.fail(protocol.ExitFailure, "%v", err)
118	}
119	return c.emit(map[string]string{"revoked": args[0]}, func(w io.Writer) {
120		fmt.Fprintf(w, "revoked %s\n", args[0])
121	})
122}