diff --git a/api/client.go b/api/client.go index 136b730a5d..80ccd7d502 100644 --- a/api/client.go +++ b/api/client.go @@ -84,6 +84,14 @@ type Config struct { // then that limiter will be used. Note that an empty Limiter // is equivalent blocking all events. Limiter *rate.Limiter + + // OutputCurlString causes the actual request to return an error of type + // *OutputStringError. Type asserting the error message will allow + // fetching a cURL-compatible string for the operation. + // + // Note: It is not thread-safe to set this and make concurrent requests + // with the same client. Cloning a client will not clone this value. + OutputCurlString bool } // TLSConfig contains the parameters needed to configure TLS on the HTTP client @@ -438,6 +446,24 @@ func (c *Client) SetClientTimeout(timeout time.Duration) { c.config.Timeout = timeout } +func (c *Client) OutputCurlString() bool { + c.modifyLock.RLock() + c.config.modifyLock.RLock() + defer c.config.modifyLock.RUnlock() + c.modifyLock.RUnlock() + + return c.config.OutputCurlString +} + +func (c *Client) SetOutputCurlString(curl bool) { + c.modifyLock.RLock() + c.config.modifyLock.Lock() + defer c.config.modifyLock.Unlock() + c.modifyLock.RUnlock() + + c.config.OutputCurlString = curl +} + // CurrentWrappingLookupFunc sets a lookup function that returns desired wrap TTLs // for a given operation and path func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc { @@ -662,6 +688,7 @@ func (c *Client) RawRequestWithContext(ctx context.Context, r *Request) (*Respon backoff := c.config.Backoff httpClient := c.config.HttpClient timeout := c.config.Timeout + outputCurlString := c.config.OutputCurlString c.config.modifyLock.RUnlock() c.modifyLock.RUnlock() @@ -688,6 +715,11 @@ START: return nil, fmt.Errorf("nil request created") } + if outputCurlString { + LastOutputStringError = &OutputStringError{Request: req} + return nil, LastOutputStringError + } + if timeout != 0 { ctx, _ = context.WithTimeout(ctx, timeout) } diff --git a/api/output_string.go b/api/output_string.go new file mode 100644 index 0000000000..ebfdad6c0d --- /dev/null +++ b/api/output_string.go @@ -0,0 +1,69 @@ +package api + +import ( + "fmt" + "strings" + + retryablehttp "github.com/hashicorp/go-retryablehttp" +) + +const ( + ErrOutputStringRequest = "output a string, please" +) + +var ( + LastOutputStringError *OutputStringError +) + +type OutputStringError struct { + *retryablehttp.Request + parsingError error + parsedCurlString string +} + +func (d *OutputStringError) Error() string { + if d.parsedCurlString == "" { + d.parseRequest() + if d.parsingError != nil { + return d.parsingError.Error() + } + } + + return ErrOutputStringRequest +} + +func (d *OutputStringError) parseRequest() { + body, err := d.Request.BodyBytes() + if err != nil { + d.parsingError = err + return + } + + // Build cURL string + d.parsedCurlString = "curl " + d.parsedCurlString = fmt.Sprintf("%s-X %s ", d.parsedCurlString, d.Request.Method) + for k, v := range d.Request.Header { + for _, h := range v { + if strings.ToLower(k) == "x-vault-token" { + h = `$(vault print token)` + } + d.parsedCurlString = fmt.Sprintf("%s-H \"%s: %s\" ", d.parsedCurlString, k, h) + } + } + + if len(body) > 0 { + // We need to escape single quotes since that's what we're using to + // quote the body + escapedBody := strings.Replace(string(body), "'", "'\"'\"'", -1) + d.parsedCurlString = fmt.Sprintf("%s-d '%s' ", d.parsedCurlString, escapedBody) + } + + d.parsedCurlString = fmt.Sprintf("%s%s", d.parsedCurlString, d.Request.URL.String()) +} + +func (d *OutputStringError) CurlString() string { + if d.parsedCurlString == "" { + d.parseRequest() + } + return d.parsedCurlString +} diff --git a/command/base.go b/command/base.go index d1773b067e..db37fd37c3 100644 --- a/command/base.go +++ b/command/base.go @@ -50,8 +50,9 @@ type BaseCommand struct { flagTLSSkipVerify bool flagWrapTTL time.Duration - flagFormat string - flagField string + flagFormat string + flagField string + flagOutputCurlString bool flagMFA []string @@ -78,6 +79,10 @@ func (c *BaseCommand) Client() (*api.Client, error) { config.Address = c.flagAddress } + if c.flagOutputCurlString { + config.OutputCurlString = c.flagOutputCurlString + } + // If we need custom TLS configuration, then set it if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" || c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify { @@ -325,6 +330,15 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { Completion: complete.PredictAnything, Usage: "Supply MFA credentials as part of X-Vault-MFA header.", }) + + f.BoolVar(&BoolVar{ + Name: "output-curl-string", + Target: &c.flagOutputCurlString, + Default: false, + Usage: "Instead of executing the request, print an equivalent cURL " + + "command string and exit.", + }) + } if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 { diff --git a/command/commands.go b/command/commands.go index c469870456..b0507e1b4e 100644 --- a/command/commands.go +++ b/command/commands.go @@ -423,6 +423,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { BaseCommand: getBaseCommand(), }, nil }, + "print token": func() (cli.Command, error) { + return &PrintTokenCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "read": func() (cli.Command, error) { return &ReadCommand{ BaseCommand: getBaseCommand(), diff --git a/command/kv_helpers.go b/command/kv_helpers.go index 2ed1d9739a..4b16c37631 100644 --- a/command/kv_helpers.go +++ b/command/kv_helpers.go @@ -46,6 +46,9 @@ func kvPreflightVersionRequest(client *api.Client, path string) (string, int, er currentWrappingLookupFunc := client.CurrentWrappingLookupFunc() client.SetWrappingLookupFunc(nil) defer client.SetWrappingLookupFunc(currentWrappingLookupFunc) + currentOutputCurlString := client.OutputCurlString() + client.SetOutputCurlString(false) + defer client.SetOutputCurlString(currentOutputCurlString) r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path) resp, err := client.RawRequest(r) diff --git a/command/main.go b/command/main.go index cf9b0b777c..a41e09af5e 100644 --- a/command/main.go +++ b/command/main.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "os" "sort" "strings" @@ -23,7 +24,7 @@ type VaultUI struct { // setupEnv parses args and may replace them and sets some env vars to known // values based on format options -func setupEnv(args []string) (retArgs []string, format string) { +func setupEnv(args []string) (retArgs []string, format string, outputCurlString bool) { var nextArgFormat bool for _, arg := range args { @@ -42,6 +43,11 @@ func setupEnv(args []string) (retArgs []string, format string) { break } + if arg == "-output-curl-string" { + outputCurlString = true + continue + } + // Parse a given flag here, which overrides the env var if strings.HasPrefix(arg, "--format=") { format = strings.TrimPrefix(arg, "--format=") @@ -66,7 +72,7 @@ func setupEnv(args []string) (retArgs []string, format string) { format = "table" } - return args, format + return args, format, outputCurlString } type RunOptions struct { @@ -89,7 +95,8 @@ func RunCustom(args []string, runOpts *RunOptions) int { } var format string - args, format = setupEnv(args) + var outputCurlString bool + args, format, outputCurlString = setupEnv(args) // Don't use color if disabled useColor := true @@ -117,13 +124,18 @@ func RunCustom(args []string, runOpts *RunOptions) int { runOpts.Stderr = colorable.NewNonColorable(runOpts.Stderr) } + uiErrWriter := runOpts.Stderr + if outputCurlString { + uiErrWriter = ioutil.Discard + } + ui := &VaultUI{ Ui: &cli.ColoredUi{ ErrorColor: cli.UiColorRed, WarnColor: cli.UiColorYellow, Ui: &cli.BasicUi{ Writer: runOpts.Stdout, - ErrorWriter: runOpts.Stderr, + ErrorWriter: uiErrWriter, }, }, format: format, @@ -168,7 +180,27 @@ func RunCustom(args []string, runOpts *RunOptions) int { } exitCode, err := cli.Run() - if err != nil { + if outputCurlString { + if exitCode == 0 { + fmt.Fprint(runOpts.Stderr, "Could not generate cURL command") + return 1 + } else { + if api.LastOutputStringError == nil { + if exitCode == 127 { + // Usage, just pass it through + return exitCode + } + fmt.Fprint(runOpts.Stderr, "cURL command not set by API operation; run without -output-curl-string to see the generated error\n") + return exitCode + } + if api.LastOutputStringError.Error() != api.ErrOutputStringRequest { + runOpts.Stdout.Write([]byte(fmt.Sprintf("Error creating request string: %s\n", api.LastOutputStringError.Error()))) + return 1 + } + runOpts.Stdout.Write([]byte(fmt.Sprintf("%s\n", api.LastOutputStringError.CurlString()))) + return 0 + } + } else if err != nil { fmt.Fprintf(runOpts.Stderr, "Error executing CLI: %s\n", err.Error()) return 1 } diff --git a/command/print_token.go b/command/print_token.go new file mode 100644 index 0000000000..a86d8bfa2a --- /dev/null +++ b/command/print_token.go @@ -0,0 +1,56 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*PrintTokenCommand)(nil) +var _ cli.CommandAutocomplete = (*PrintTokenCommand)(nil) + +type PrintTokenCommand struct { + *BaseCommand +} + +func (c *PrintTokenCommand) Synopsis() string { + return "Prints the contents of a policy" +} + +func (c *PrintTokenCommand) Help() string { + helpText := ` +Usage: vault print token + + Prints the value of the Vault token that will be used for commands, after + taking into account the configured token-helper and the environment. + + $ vault print token + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *PrintTokenCommand) Flags() *FlagSets { + return nil +} + +func (c *PrintTokenCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *PrintTokenCommand) AutocompleteFlags() complete.Flags { + return nil +} + +func (c *PrintTokenCommand) Run(args []string) int { + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + c.UI.Output(client.Token()) + return 0 +}