diff --git a/command/token_create.go b/command/token_create.go index f8d8c59265..ca5b7752f7 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -3,174 +3,250 @@ package command import ( "fmt" "strings" + "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/flag-kv" - "github.com/hashicorp/vault/helper/flag-slice" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// 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. type TokenCreateCommand struct { - meta.Meta -} + *BaseCommand -func (c *TokenCreateCommand) Run(args []string) int { - var format string - var id, displayName, lease, ttl, explicitMaxTTL, period, role string - var orphan, noDefaultPolicy, renewable bool - var metadata map[string]string - var numUses int - var policies []string - flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&displayName, "display-name", "", "") - flags.StringVar(&id, "id", "", "") - flags.StringVar(&lease, "lease", "", "") - 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 - } + flagID string + flagDisplayName string + flagTTL time.Duration + flagExplicitMaxTTL time.Duration + flagPeriod time.Duration + flagRenewable bool + flagOrphan bool + flagNoDefaultPolicy bool + flagUseLimit int + flagRole string + flagMetadata map[string]string + flagPolicies []string - args = flags.Args() - if len(args) != 0 { - 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) + // Deprecated flags + flagLease time.Duration } func (c *TokenCreateCommand) Synopsis() string { - return "Create a new auth token" + return "Creates a new token" } func (c *TokenCreateCommand) Help() string { helpText := ` 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. - This token will be created as a child of your token. The created token - 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 + A ttl can also be associated with the token. If a ttl is not associated + with the token, then it cannot be renewed. If a ttl is associated with the token, it will expire after that amount of time unless it is renewed. - Metadata associated with the token (specified with "-metadata") is - written to the audit log when the token is used. + Metadata associated with the token (specified with "-metadata") is written + to the audit log when the token is used. If a role is specified, the role may override parameters specified here. -General Options: -` + meta.GeneralOptionsUsage() + ` -Token Options: +` + c.Flags().Help() - -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/). -` 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/\".", + }) + + 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) +} diff --git a/command/token_create_test.go b/command/token_create_test.go index 9db2a26a29..ec0cc79fb5 100644 --- a/command/token_create_test.go +++ b/command/token_create_test.go @@ -1,38 +1,243 @@ package command import ( + "reflect" "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestTokenCreate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testTokenCreateCommand(tb testing.TB) (*cli.MockUi, *TokenCreateCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &TokenCreateCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &TokenCreateCommand{ + BaseCommand: &BaseCommand{ + 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{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Ensure we get lease info - output := ui.OutputWriter.String() - if !strings.Contains(output, "token_duration") { - t.Fatalf("bad: %#v", output) - } + for _, tc := range cases { + tc := tc + + 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) + }) }