internal/control/token.go

1ed9fb9399b21da8e6cf45be8389792aac82d5bc
gitbay/internal/control/token.go history · blame · raw

183 lines · 5551 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 read|full] [--ttl 30d|720h]",
 19		Flags: []Flag{
 20			{"--name", "<n>", "the token's name", ""},
 21			{"--scope", "read|full", "what the token may do; full is needed to change anything", "read"},
 22			{"--ttl", "30d|720h", "how long the token is valid; an expiring token cannot mint credentials", "never expires"},
 23		},
 24		Examples:        []string{"token create --name laptop --ttl 30d", "token create --name phone --scope full"},
 25		MintsCredential: true,
 26		Run:             runTokenCreate})
 27	register(Command{Path: []string{"token", "list"},
 28		Summary:  "list API tokens",
 29		Usage:    "token list",
 30		Examples: []string{"token list"}, ReadOnly: true, Run: runTokenList})
 31	register(Command{Path: []string{"token", "revoke"},
 32		Summary: "revoke an API token by name",
 33		Usage:   "token revoke <name> [--created]",
 34		Flags: []Flag{
 35			{"--created", "", "also revoke the tokens and keys it created, at any depth", ""},
 36		},
 37		Examples: []string{"token revoke laptop", "token revoke laptop --created"},
 38		Run:      runTokenRevoke})
 39}
 40
 41// parseTTL accepts Go durations plus a day suffix ("30d").
 42func parseTTL(s string) (time.Duration, error) {
 43	if days, ok := strings.CutSuffix(s, "d"); ok {
 44		n, err := strconv.Atoi(days)
 45		if err != nil || n < 1 {
 46			return 0, fmt.Errorf("bad ttl %q", s)
 47		}
 48		return time.Duration(n) * 24 * time.Hour, nil
 49	}
 50	return time.ParseDuration(s)
 51}
 52
 53// ttlFlag reads --ttl as an expiry; nil when the flag is absent. The
 54// code is -1 when the caller may go on.
 55func (c *Ctx) ttlFlag(f flags) (*time.Time, int) {
 56	if !f.Has("--ttl") {
 57		return nil, -1
 58	}
 59	d, err := parseTTL(f.Value("--ttl"))
 60	if err != nil || d <= 0 {
 61		return nil, c.fail(protocol.ExitUsage, "bad ttl %q: give a duration such as 30d or 720h", f.Value("--ttl"))
 62	}
 63	t := time.Now().Add(d)
 64	return &t, -1
 65}
 66
 67func runTokenCreate(c *Ctx, args []string) int {
 68	f, err := parseFlags(args, flagSpec{Values: []string{"--name", "--scope", "--ttl"}, MaxPos: 0, Usage: c.Cmd.Usage})
 69	if err != nil {
 70		return c.fail(protocol.ExitUsage, "%v", err)
 71	}
 72	name, scope, ttl := f.Value("--name"), "read", f.Value("--ttl")
 73	if f.Has("--scope") {
 74		scope = f.Value("--scope")
 75	}
 76	if name == "" || (scope != "full" && scope != "read") {
 77		return c.usage()
 78	}
 79	var expires *time.Time
 80	if ttl != "" {
 81		d, err := parseTTL(ttl)
 82		if err != nil {
 83			return c.failInput(err)
 84		}
 85		t := time.Now().Add(d)
 86		expires = &t
 87	}
 88	raw, _, err := store.NewToken()
 89	if err != nil {
 90		return c.fail(protocol.ExitFailure, "%v", err)
 91	}
 92	// The gb_ prefix makes leaked tokens findable by secret scanners.
 93	token := "gb_" + raw
 94	if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires, c.TokenID); err != nil {
 95		return c.failErr(err)
 96	}
 97	type out struct {
 98		Name  string `json:"name"`
 99		Scope string `json:"scope"`
100		Token string `json:"token"`
101	}
102	d := out{name, scope, token}
103	return c.emit(d, func(w io.Writer) {
104		fmt.Fprintf(w, "token %q (%s) — shown once, store it now:\n%s\n", d.Name, d.Scope, d.Token)
105	})
106}
107
108func runTokenList(c *Ctx, args []string) int {
109	tokens, err := c.Store.ListAPITokens(c.User.ID)
110	if err != nil {
111		return c.fail(protocol.ExitFailure, "%v", err)
112	}
113	type out struct {
114		Name       string     `json:"name"`
115		Scope      string     `json:"scope"`
116		CreatedAt  string     `json:"created_at"`
117		ExpiresAt  *time.Time `json:"expires_at,omitempty"`
118		LastUsedAt *time.Time `json:"last_used_at,omitempty"`
119		CreatedBy  string     `json:"created_by,omitempty"`
120	}
121	var ds []out
122	for _, t := range tokens {
123		ds = append(ds, out{t.Name, t.Scope, t.CreatedAt, t.ExpiresAt, t.LastUsedAt, t.CreatedBy})
124	}
125	return c.emit(ds, func(w io.Writer) {
126		tb := c.table(w, "NAME", "SCOPE", "EXPIRES")
127		for _, d := range ds {
128			exp := "never expires"
129			if d.ExpiresAt != nil {
130				ts := d.ExpiresAt.UTC().Format(time.RFC3339Nano)
131				if c.Term.Cols == 0 {
132					exp = "expires " + stamp(ts)
133				} else {
134					exp = "expires " + relAge(ts, termNow())
135				}
136			}
137			tb.row(cRef(d.Name), cState(d.Scope), cText(exp))
138		}
139		tb.flush()
140	})
141}
142
143func runTokenRevoke(c *Ctx, args []string) int {
144	f, err := parseFlags(args, flagSpec{Bools: []string{"--created"}, MaxPos: 1, Usage: c.Cmd.Usage})
145	if err != nil {
146		return c.fail(protocol.ExitUsage, "%v", err)
147	}
148	name := f.pos(0)
149	if name == "" {
150		return c.usage()
151	}
152	withCreated := f.Has("--created")
153	created, err := c.Store.RevokeAPIToken(c.User.ID, name, withCreated)
154	if err != nil {
155		if errors.Is(err, store.ErrNotFound) {
156			return c.fail(protocol.ExitNotFound, "no token named %q", name)
157		}
158		return c.fail(protocol.ExitFailure, "%v", err)
159	}
160	type out struct {
161		Revoked        string        `json:"revoked"`
162		Created        store.Created `json:"created"`
163		CreatedRevoked bool          `json:"created_revoked"`
164	}
165	d := out{name, created, withCreated}
166	return c.emit(d, func(w io.Writer) {
167		fmt.Fprintf(w, "revoked %s\n", name)
168		if len(created.Tokens)+len(created.Keys) == 0 {
169			return
170		}
171		if withCreated {
172			fmt.Fprintln(w, "and what it created:")
173		} else {
174			fmt.Fprintln(w, "it created these, still in place:")
175		}
176		for _, n := range created.Tokens {
177			fmt.Fprintf(w, "  token %s\n", n)
178		}
179		for _, fp := range created.Keys {
180			fmt.Fprintf(w, "  key %s\n", fp)
181		}
182	})
183}