Add token rotation
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"send/internal/auth"
|
||||
"send/internal/config"
|
||||
@@ -20,6 +21,8 @@ const tokenUsage = `send token - manage upload credentials
|
||||
Usage:
|
||||
send token add <name> [options] generate a token
|
||||
send token add <name> --token - read a chosen one from stdin
|
||||
send token rotate <name> [--token -] replace the secret, keep everything else
|
||||
send token set <name> [options] change limits and flags in place
|
||||
send token list [options]
|
||||
send token rm <name> [options]
|
||||
|
||||
@@ -70,14 +73,26 @@ func tokenCommand(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
warnIfUnusedDataDir(opts.dataDir, opts.tokensPath)
|
||||
|
||||
switch sub {
|
||||
case "add":
|
||||
if name == "" {
|
||||
return errors.New("token add: a name is required")
|
||||
}
|
||||
// Only worth saying when a file is about to be created somewhere new.
|
||||
// Every other subcommand reports a wrong --data on its own, by finding
|
||||
// no such token or an empty list.
|
||||
warnIfUnusedDataDir(opts.dataDir, opts.tokensPath)
|
||||
return tokenAdd(file, name, opts)
|
||||
case "rotate":
|
||||
if name == "" {
|
||||
return errors.New("token rotate: a name is required")
|
||||
}
|
||||
return tokenRotate(file, name, opts)
|
||||
case "set":
|
||||
if name == "" {
|
||||
return errors.New("token set: a name is required")
|
||||
}
|
||||
return tokenSet(file, name, opts, fs)
|
||||
case "list":
|
||||
return tokenList(file)
|
||||
case "rm", "remove", "delete":
|
||||
@@ -203,6 +218,98 @@ func tokenAdd(file *auth.File, name string, opts tokenOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenRotate replaces a token's secret while keeping its name, limits, flags
|
||||
// and history. Anyone still holding the old secret, in a script or in a browser
|
||||
// session, stops being authenticated the moment this returns.
|
||||
func tokenRotate(file *auth.File, name string, opts tokenOptions) error {
|
||||
chosen := opts.chosen
|
||||
if chosen == "-" {
|
||||
read, err := readSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chosen = read
|
||||
}
|
||||
|
||||
var generated string
|
||||
err := file.Update(name, func(t *auth.Token) error {
|
||||
if chosen != "" {
|
||||
return t.SetChosen(chosen)
|
||||
}
|
||||
secret, err := t.SetGenerated()
|
||||
generated = secret
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Rotated token %q in %s\n\n", name, file.Path())
|
||||
if generated != "" {
|
||||
fmt.Printf(" %s\n\n", generated)
|
||||
fmt.Println("This is the only time it is shown; only its hash is stored.")
|
||||
}
|
||||
fmt.Println("Anything still using the old secret is now refused, including")
|
||||
fmt.Println("browser sessions, which will have to log in again.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenSet changes limits and flags in place. Only the options actually given
|
||||
// on the command line are applied, so there is no way to reset something by
|
||||
// forgetting to mention it.
|
||||
func tokenSet(file *auth.File, name string, opts tokenOptions, fs *config.Set) error {
|
||||
var changes []string
|
||||
err := file.Update(name, func(t *auth.Token) error {
|
||||
for _, f := range []struct {
|
||||
flag string
|
||||
value string
|
||||
dst **string
|
||||
}{
|
||||
{"max-size", opts.maxSize, &t.MaxSize},
|
||||
{"max-expiry", opts.maxExpiry, &t.MaxExpiry},
|
||||
{"default-expiry", opts.defExpiry, &t.DefaultExpiry},
|
||||
} {
|
||||
if !fs.Changed(f.flag) {
|
||||
continue
|
||||
}
|
||||
if f.value == "" {
|
||||
*f.dst = nil // back to inheriting the server default
|
||||
changes = append(changes, "--"+f.flag+" (inherited)")
|
||||
continue
|
||||
}
|
||||
v := f.value
|
||||
*f.dst = &v
|
||||
changes = append(changes, "--"+f.flag+" "+v)
|
||||
}
|
||||
for _, f := range []struct {
|
||||
flag string
|
||||
value bool
|
||||
dst *bool
|
||||
}{
|
||||
{"vanity", opts.vanity, &t.AllowVanity},
|
||||
{"admin", opts.admin, &t.Admin},
|
||||
} {
|
||||
if !fs.Changed(f.flag) {
|
||||
continue
|
||||
}
|
||||
*f.dst = f.value
|
||||
changes = append(changes, fmt.Sprintf("--%s=%t", f.flag, f.value))
|
||||
}
|
||||
if fs.Changed("token") {
|
||||
return errors.New("use \"send token rotate\" to change the secret")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return errors.New("token set: nothing to change; give at least one option")
|
||||
}
|
||||
fmt.Printf("Updated token %q: %s\n", name, strings.Join(changes, ", "))
|
||||
return nil
|
||||
}
|
||||
|
||||
// readSecret reads a token from standard input, so it need not appear in a
|
||||
// shell history or in the process list.
|
||||
func readSecret() (string, error) {
|
||||
@@ -224,13 +331,13 @@ func tokenList(file *auth.File) error {
|
||||
return nil
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "NAME\tKIND\tMAX SIZE\tMAX EXPIRY\tDEFAULT\tVANITY\tADMIN\tCREATED")
|
||||
fmt.Fprintln(w, "NAME\tKIND\tMAX SIZE\tMAX EXPIRY\tDEFAULT\tVANITY\tADMIN\tCREATED\tROTATED")
|
||||
for _, t := range tokens {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
t.Name, kind(t),
|
||||
inherited(t.MaxSize), inherited(t.MaxExpiry), inherited(t.DefaultExpiry),
|
||||
yesNo(t.AllowVanity), yesNo(t.Admin),
|
||||
t.Created.Format("2006-01-02"))
|
||||
t.Created.Format("2006-01-02"), date(t.Rotated))
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
@@ -242,6 +349,13 @@ func kind(t *auth.Token) string {
|
||||
return "generated"
|
||||
}
|
||||
|
||||
func date(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "never"
|
||||
}
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func inherited(s *string) string {
|
||||
if s == nil {
|
||||
return "(default)"
|
||||
|
||||
Reference in New Issue
Block a user