Update token-create command

This commit is contained in:
Seth Vargo
2017-09-05 00:04:54 -04:00
parent ba5712ef4f
commit eee5edb102
2 changed files with 449 additions and 168 deletions

View File

@@ -3,174 +3,250 @@ package command
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/flag-kv" "github.com/mitchellh/cli"
"github.com/hashicorp/vault/helper/flag-slice" "github.com/posener/complete"
"github.com/hashicorp/vault/meta"
) )
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*TokenCreateCommand)(nil)
var _ cli.CommandAutocomplete = (*TokenCreateCommand)(nil)
// TokenCreateCommand is a Command that mounts a new mount. // TokenCreateCommand is a Command that mounts a new mount.
type TokenCreateCommand struct { type TokenCreateCommand struct {
meta.Meta *BaseCommand
}
func (c *TokenCreateCommand) Run(args []string) int { flagID string
var format string flagDisplayName string
var id, displayName, lease, ttl, explicitMaxTTL, period, role string flagTTL time.Duration
var orphan, noDefaultPolicy, renewable bool flagExplicitMaxTTL time.Duration
var metadata map[string]string flagPeriod time.Duration
var numUses int flagRenewable bool
var policies []string flagOrphan bool
flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) flagNoDefaultPolicy bool
flags.StringVar(&format, "format", "table", "") flagUseLimit int
flags.StringVar(&displayName, "display-name", "", "") flagRole string
flags.StringVar(&id, "id", "", "") flagMetadata map[string]string
flags.StringVar(&lease, "lease", "", "") flagPolicies []string
flags.StringVar(&ttl, "ttl", "", "")
flags.StringVar(&explicitMaxTTL, "explicit-max-ttl", "", "")
flags.StringVar(&period, "period", "", "")
flags.StringVar(&role, "role", "", "")
flags.BoolVar(&orphan, "orphan", false, "")
flags.BoolVar(&renewable, "renewable", true, "")
flags.BoolVar(&noDefaultPolicy, "no-default-policy", false, "")
flags.IntVar(&numUses, "use-limit", 0, "")
flags.Var((*kvFlag.Flag)(&metadata), "metadata", "")
flags.Var((*sliceflag.StringFlag)(&policies), "policy", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args() // Deprecated flags
if len(args) != 0 { flagLease time.Duration
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\ntoken-create expects no arguments"))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if ttl == "" {
ttl = lease
}
tcr := &api.TokenCreateRequest{
ID: id,
Policies: policies,
Metadata: metadata,
TTL: ttl,
NoParent: orphan,
NoDefaultPolicy: noDefaultPolicy,
DisplayName: displayName,
NumUses: numUses,
Renewable: new(bool),
ExplicitMaxTTL: explicitMaxTTL,
Period: period,
}
*tcr.Renewable = renewable
var secret *api.Secret
if role != "" {
secret, err = client.Auth().Token().CreateWithRole(tcr, role)
} else {
secret, err = client.Auth().Token().Create(tcr)
}
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error creating token: %s", err))
return 2
}
return OutputSecret(c.Ui, format, secret)
} }
func (c *TokenCreateCommand) Synopsis() string { func (c *TokenCreateCommand) Synopsis() string {
return "Create a new auth token" return "Creates a new token"
} }
func (c *TokenCreateCommand) Help() string { func (c *TokenCreateCommand) Help() string {
helpText := ` helpText := `
Usage: vault token-create [options] Usage: vault token-create [options]
Create a new auth token. Creates a new token that can be used for authentication. This token will be
created as a child of the currently authenticated token. The generated token
will inherit all policies and permissions of the currently authenticated
token unless you explicitly define a subset list policies to assign to the
token.
This command creates a new token that can be used for authentication. A ttl can also be associated with the token. If a ttl is not associated
This token will be created as a child of your token. The created token with the token, then it cannot be renewed. If a ttl is associated with
will inherit your policies, or can be assigned a subset of your policies.
A lease can also be associated with the token. If a lease is not associated
with the token, then it cannot be renewed. If a lease is associated with
the token, it will expire after that amount of time unless it is renewed. the token, it will expire after that amount of time unless it is renewed.
Metadata associated with the token (specified with "-metadata") is Metadata associated with the token (specified with "-metadata") is written
written to the audit log when the token is used. to the audit log when the token is used.
If a role is specified, the role may override parameters specified here. If a role is specified, the role may override parameters specified here.
General Options: ` + c.Flags().Help()
` + meta.GeneralOptionsUsage() + `
Token Options:
-id="7699125c-d8...." The token value that clients will use to authenticate
with Vault. If not provided this defaults to a 36
character UUID. A root token is required to specify
the ID of a token.
-display-name="name" A display name to associate with this token. This
is a non-security sensitive value used to help
identify created secrets, i.e. prefixes.
-ttl="1h" Initial TTL to associate with the token; renewals can
extend this value.
-explicit-max-ttl="1h" An explicit maximum lifetime for the token. Unlike
normal token TTLs, which can be renewed up until the
maximum TTL set on the auth/token mount or the system
configuration file, this lifetime is a hard limit set
on the token itself and cannot be exceeded.
-period="1h" If specified, the token will be periodic; it will
have no maximum TTL (unless an "explicit-max-ttl" is
also set) but every renewal will use the given
period. Requires a root/sudo token to use.
-renewable=true Whether or not the token is renewable to extend its
TTL up to Vault's configured maximum TTL for tokens.
This defaults to true; set to false to disable
renewal of this token.
-metadata="key=value" Metadata to associate with the token. This shows
up in the audit log. This can be specified multiple
times.
-orphan If specified, the token will have no parent. This
prevents the new token from being revoked with
your token. Requires a root/sudo token to use.
-no-default-policy If specified, the token will not have the "default"
policy included in its policy set.
-policy="name" Policy to associate with this token. This can be
specified multiple times.
-use-limit=5 The number of times this token can be used until
it is automatically revoked.
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json or yaml.
-role=name If set, the token will be created against the named
role. The role may override other parameters. This
requires the client to have permissions on the
appropriate endpoint (auth/token/create/<name>).
`
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
func (c *TokenCreateCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "id",
Target: &c.flagID,
Completion: complete.PredictAnything,
Usage: "Value for the token. By default, this is an auto-generated 36 " +
"character UUID. Specifying this value requires sudo permissions.",
})
f.StringVar(&StringVar{
Name: "display-name",
Target: &c.flagDisplayName,
Completion: complete.PredictAnything,
Usage: "Name to associate with this token. This is a non-sensitive value " +
"that can be used to help identify created secrets (e.g. prefixes).",
})
f.DurationVar(&DurationVar{
Name: "ttl",
Target: &c.flagTTL,
Completion: complete.PredictAnything,
Usage: "Initial TTL to associate with the token. Token renewals may be " +
"able to extend beyond this value, depending on the configured maximum" +
"TTLs. This is specified as a numeric string with suffix like \"30s\" " +
"or \"5m\".",
})
f.DurationVar(&DurationVar{
Name: "explicit-max-ttl",
Target: &c.flagExplicitMaxTTL,
Completion: complete.PredictAnything,
Usage: "Explicit maximum lifetime for the token. Unlike normal TTLs, the " +
"maximum TTL is a hard limit and cannot be exceeded. This is specified " +
"as a numeric string with suffix like \"30s\" or \"5m\".",
})
f.DurationVar(&DurationVar{
Name: "period",
Target: &c.flagPeriod,
Completion: complete.PredictAnything,
Usage: "If specified, every renewal will use the given period. Periodic " +
"tokens do not expire (unless -explicit-max-ttl is also provided). " +
"Setting this value requires sudo permissions. This is specified as a " +
"numeric string with suffix like \"30s\" or \"5m\".",
})
f.BoolVar(&BoolVar{
Name: "renewable",
Target: &c.flagRenewable,
Default: true,
Usage: "Allow the token to be renewed up to it's maximum TTL.",
})
f.BoolVar(&BoolVar{
Name: "orphan",
Target: &c.flagOrphan,
Default: false,
Usage: "Create the token with no parent. This prevents the token from " +
"being revoked when the token which created it expires. Setting this " +
"value requires sudo permissions.",
})
f.BoolVar(&BoolVar{
Name: "no-default-policy",
Target: &c.flagNoDefaultPolicy,
Default: false,
Usage: "Detach the \"default\" policy from the policy set for this " +
"token.",
})
f.IntVar(&IntVar{
Name: "use-limit",
Target: &c.flagUseLimit,
Default: 0,
Usage: "Number of times this token can be used. After the last use, the " +
"token is automatically revoked. By default, tokens can be used an " +
"unlimited number of times until their expiration.",
})
f.StringVar(&StringVar{
Name: "role",
Target: &c.flagRole,
Default: "",
Usage: "Name of the role to create the token against. Specifying -role " +
"may override other arguments. The locally authenticated Vault token " +
"must have permission for \"auth/token/create/<role>\".",
})
f.StringMapVar(&StringMapVar{
Name: "metadata",
Target: &c.flagMetadata,
Completion: complete.PredictAnything,
Usage: "Arbitary key=value metadata to associate with the token. " +
"This metadata will show in the audit log when the token is used. " +
"This can be specified multiple times to add multiple pieces of " +
"metadata.",
})
f.StringSliceVar(&StringSliceVar{
Name: "policy",
Target: &c.flagPolicies,
Completion: c.PredictVaultPolicies(),
Usage: "Name of a policy to associate with this token. This can be " +
"specified multiple times to attach multiple policies.",
})
// Deprecated flags
// TODO: remove in 0.9.0
f.DurationVar(&DurationVar{
Name: "lease", // prefer -ttl
Target: &c.flagLease,
Default: 0,
Hidden: true,
})
return set
}
func (c *TokenCreateCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *TokenCreateCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *TokenCreateCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
// TODO: remove in 0.9.0
if c.flagLease != 0 {
c.UI.Warn("The -lease flag is deprecated. Please use -ttl instead.")
c.flagTTL = c.flagLease
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
tcr := &api.TokenCreateRequest{
ID: c.flagID,
Policies: c.flagPolicies,
Metadata: c.flagMetadata,
TTL: c.flagTTL.String(),
NoParent: c.flagOrphan,
NoDefaultPolicy: c.flagNoDefaultPolicy,
DisplayName: c.flagDisplayName,
NumUses: c.flagUseLimit,
Renewable: &c.flagRenewable,
ExplicitMaxTTL: c.flagExplicitMaxTTL.String(),
Period: c.flagPeriod.String(),
}
var secret *api.Secret
if c.flagRole != "" {
secret, err = client.Auth().Token().CreateWithRole(tcr, c.flagRole)
} else {
secret, err = client.Auth().Token().Create(tcr)
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating token: %s", err))
return 2
}
if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}
return OutputSecret(c.UI, c.flagFormat, secret)
}

View File

@@ -1,38 +1,243 @@
package command package command
import ( import (
"reflect"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func TestTokenCreate(t *testing.T) { func testTokenCreateCommand(tb testing.TB) (*cli.MockUi, *TokenCreateCommand) {
core, _, token := vault.TestCoreUnsealed(t) tb.Helper()
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi) ui := cli.NewMockUi()
c := &TokenCreateCommand{ return ui, &TokenCreateCommand{
Meta: meta.Meta{ BaseCommand: &BaseCommand{
ClientToken: token, UI: ui,
Ui: ui, },
}
}
func TestTokenCreateCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"abcd1234"},
"Too many arguments",
1,
},
{
"default",
nil,
"token",
0,
},
{
"metadata",
[]string{"-metadata", "foo=bar", "-metadata", "zip=zap"},
"token",
0,
},
{
"policies",
[]string{"-policy", "foo", "-policy", "bar"},
"token",
0,
},
{
"field",
[]string{
"-field", "token_renewable",
},
"false",
0,
},
{
"field_not_found",
[]string{
"-field", "not-a-real-field",
},
"not present in secret",
1,
},
{
"format",
[]string{
"-format", "json",
},
"{",
0,
},
{
"format_bad",
[]string{
"-format", "nope-not-real",
},
"Invalid output format",
1,
}, },
} }
args := []string{ t.Run("validations", func(t *testing.T) {
"-address", addr, t.Parallel()
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Ensure we get lease info for _, tc := range cases {
output := ui.OutputWriter.String() tc := tc
if !strings.Contains(output, "token_duration") {
t.Fatalf("bad: %#v", output) t.Run(tc.name, func(t *testing.T) {
} t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testTokenCreateCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("default", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testTokenCreateCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-field", "token",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
token := strings.TrimSpace(ui.OutputWriter.String())
secret, err := client.Auth().Token().Lookup(token)
if secret == nil || err != nil {
t.Fatal(err)
}
})
t.Run("metadata", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testTokenCreateCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-metadata", "foo=bar",
"-metadata", "zip=zap",
"-field", "token",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
token := strings.TrimSpace(ui.OutputWriter.String())
secret, err := client.Auth().Token().Lookup(token)
if secret == nil || err != nil {
t.Fatal(err)
}
meta, ok := secret.Data["meta"].(map[string]interface{})
if !ok {
t.Fatalf("missing meta: %#v", secret)
}
if _, ok := meta["foo"]; !ok {
t.Errorf("missing meta.foo: %#v", meta)
}
if _, ok := meta["zip"]; !ok {
t.Errorf("missing meta.bar: %#v", meta)
}
})
t.Run("policies", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testTokenCreateCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-policy", "foo",
"-policy", "bar",
"-field", "token",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
token := strings.TrimSpace(ui.OutputWriter.String())
secret, err := client.Auth().Token().Lookup(token)
if secret == nil || err != nil {
t.Fatal(err)
}
raw, ok := secret.Data["policies"].([]interface{})
if !ok {
t.Fatalf("missing policies: %#v", secret)
}
policies := make([]string, len(raw))
for i := range raw {
policies[i] = raw[i].(string)
}
expected := []string{"bar", "default", "foo"}
if !reflect.DeepEqual(policies, expected) {
t.Errorf("expected %q to be %q", policies, expected)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testTokenCreateCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error creating token: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testTokenCreateCommand(t)
assertNoTabs(t, cmd)
})
} }