internal/control/token.go

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

137 lines · 4043 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		Flags: []Flag{
 20			{"--name", "<n>", "the token's name", ""},
 21			{"--scope", "full|read", "what the token may do", "full"},
 22			{"--ttl", "30d|720h", "how long the token is valid", "never expires"},
 23		},
 24		Examples: []string{"token create --name laptop --scope read --ttl 30d"},
 25		Run:      runTokenCreate})
 26	register(Command{Path: []string{"token", "list"},
 27		Summary:  "list API tokens",
 28		Usage:    "token list",
 29		Examples: []string{"token list"}, ReadOnly: true, Run: runTokenList})
 30	register(Command{Path: []string{"token", "revoke"},
 31		Summary:  "revoke an API token by name",
 32		Usage:    "token revoke <name>",
 33		Examples: []string{"token revoke laptop"},
 34		Run:      runTokenRevoke})
 35}
 36
 37// parseTTL accepts Go durations plus a day suffix ("30d").
 38func parseTTL(s string) (time.Duration, error) {
 39	if days, ok := strings.CutSuffix(s, "d"); ok {
 40		n, err := strconv.Atoi(days)
 41		if err != nil || n < 1 {
 42			return 0, fmt.Errorf("bad ttl %q", s)
 43		}
 44		return time.Duration(n) * 24 * time.Hour, nil
 45	}
 46	return time.ParseDuration(s)
 47}
 48
 49func runTokenCreate(c *Ctx, args []string) int {
 50	f, err := parseFlags(args, flagSpec{Values: []string{"--name", "--scope", "--ttl"}, MaxPos: 0, Usage: "token create --name <n> [--scope full|read] [--ttl 30d]"})
 51	if err != nil {
 52		return c.fail(protocol.ExitUsage, "%v", err)
 53	}
 54	name, scope, ttl := f.Value("--name"), "full", f.Value("--ttl")
 55	if f.Has("--scope") {
 56		scope = f.Value("--scope")
 57	}
 58	if name == "" || (scope != "full" && scope != "read") {
 59		return c.usage()
 60	}
 61	var expires *time.Time
 62	if ttl != "" {
 63		d, err := parseTTL(ttl)
 64		if err != nil {
 65			return c.failInput(err)
 66		}
 67		t := time.Now().Add(d)
 68		expires = &t
 69	}
 70	raw, _, err := store.NewToken()
 71	if err != nil {
 72		return c.fail(protocol.ExitFailure, "%v", err)
 73	}
 74	// The gb_ prefix makes leaked tokens findable by secret scanners.
 75	token := "gb_" + raw
 76	if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires); err != nil {
 77		return c.failErr(err)
 78	}
 79	type out struct {
 80		Name  string `json:"name"`
 81		Scope string `json:"scope"`
 82		Token string `json:"token"`
 83	}
 84	d := out{name, scope, token}
 85	return c.emit(d, func(w io.Writer) {
 86		fmt.Fprintf(w, "token %q (%s) — shown once, store it now:\n%s\n", d.Name, d.Scope, d.Token)
 87	})
 88}
 89
 90func runTokenList(c *Ctx, args []string) int {
 91	tokens, err := c.Store.ListAPITokens(c.User.ID)
 92	if err != nil {
 93		return c.fail(protocol.ExitFailure, "%v", err)
 94	}
 95	type out struct {
 96		Name       string     `json:"name"`
 97		Scope      string     `json:"scope"`
 98		CreatedAt  string     `json:"created_at"`
 99		ExpiresAt  *time.Time `json:"expires_at,omitempty"`
100		LastUsedAt *time.Time `json:"last_used_at,omitempty"`
101	}
102	var ds []out
103	for _, t := range tokens {
104		ds = append(ds, out{t.Name, t.Scope, t.CreatedAt, t.ExpiresAt, t.LastUsedAt})
105	}
106	return c.emit(ds, func(w io.Writer) {
107		tb := c.table(w, "NAME", "SCOPE", "EXPIRES")
108		for _, d := range ds {
109			exp := "never expires"
110			if d.ExpiresAt != nil {
111				ts := d.ExpiresAt.UTC().Format(time.RFC3339Nano)
112				if c.Term.Cols == 0 {
113					exp = "expires " + stamp(ts)
114				} else {
115					exp = "expires " + relAge(ts, termNow())
116				}
117			}
118			tb.row(cRef(d.Name), cState(d.Scope), cText(exp))
119		}
120		tb.flush()
121	})
122}
123
124func runTokenRevoke(c *Ctx, args []string) int {
125	if len(args) != 1 {
126		return c.usage()
127	}
128	if err := c.Store.RevokeAPIToken(c.User.ID, args[0]); err != nil {
129		if errors.Is(err, store.ErrNotFound) {
130			return c.fail(protocol.ExitNotFound, "no token named %q", args[0])
131		}
132		return c.fail(protocol.ExitFailure, "%v", err)
133	}
134	return c.emit(map[string]string{"revoked": args[0]}, func(w io.Writer) {
135		fmt.Fprintf(w, "revoked %s\n", args[0])
136	})
137}