Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to print curl commands from CLI #6113

Merged
merged 9 commits into from
Feb 1, 2019
32 changes: 32 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
Expand Down
69 changes: 69 additions & 0 deletions api/output_string.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to redact known-sensitive headers such as X-Vault-Token?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, when it comes down to it if someone is sitting at your terminal they can easily get your token other ways. We could maybe require they put the token in an env var but it kind of breaks the point.

Maybe I'll just enhance the flag doc to explicitly mention it will do this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added a commit that changes things a little, and I'm curious about feedback. Instead of printing the token, I added a vault print token command (although maybe if we think there might be more things like this coming up it should be vault debug print-token or so), and the curl string now uses that in a bash escape.

Three items for feedback:

  1. This will work with bash and zsh but not fish. Fish doesn't use backticks either, so...I guess if someone is interested they can submit a PR to check for the running shell env var and change up the output. It also obviously does not work on Windows; same story there, if people want it they could submit a PR to modify the output based on GOOS

  2. Is this even a good idea? I'm not sure it's a bad idea. There are plenty of ways to fetch the Vault token: read the env var, read the file, or if you are using an external token helper this will still require any extra authentication to be performed as with a normal Vault call. You could also, on a machine with a connection to Vault and a normal default policy, call vault token lookup and get it. On the flip side, doing it this way means the curl commands are emailable/portable -- you can send someone the line without having to do substitution. So I think it's a net neutral or positive.

  3. vault print token vs. vault debug print-token. Depends what we think we might have in the future. If we think we might have debuggish type stuff the latter makes sense. If we think we'll have more things to specifically print later, the former makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the approach as you've implemented it. Keeps the token out of the history but output is still generally copy-pasteable. I think I prefer vault print token, as this feels like a generally useful capability, especially when token helpers are in the mix.

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
}
18 changes: 16 additions & 2 deletions command/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ type BaseCommand struct {
flagTLSSkipVerify bool
flagWrapTTL time.Duration

flagFormat string
flagField string
flagFormat string
flagField string
flagOutputCurlString bool

flagMFA []string

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions command/kv_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 37 additions & 5 deletions command/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
Expand All @@ -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 {
Expand All @@ -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=")
Expand All @@ -66,7 +72,7 @@ func setupEnv(args []string) (retArgs []string, format string) {
format = "table"
}

return args, format
return args, format, outputCurlString
}

type RunOptions struct {
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
56 changes: 56 additions & 0 deletions command/print_token.go
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 23 additions & 1 deletion vendor/github.com/hashicorp/go-retryablehttp/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.