From 0b59a54837f0cda16e2a9fe40f2c822084ca6311 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 19 May 2016 11:25:15 -0400 Subject: [PATCH] Add unwrap command, and change how the response is embedded (as a string, not an object) --- cli/commands.go | 6 ++ cli/help.go | 1 + command/read.go | 20 +----- command/unwrap.go | 133 ++++++++++++++++++++++++++++++++++++++ command/util.go | 46 ++++++++++++- vault/request_handling.go | 16 ++++- 6 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 command/unwrap.go diff --git a/cli/commands.go b/cli/commands.go index a2f84a691b..c6f8d901be 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -171,6 +171,12 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { }, nil }, + "unwrap": func() (cli.Command, error) { + return &command.UnwrapCommand{ + Meta: *metaPtr, + }, nil + }, + "list": func() (cli.Command, error) { return &command.ListCommand{ Meta: *metaPtr, diff --git a/cli/help.go b/cli/help.go index b614212c4f..620d295e21 100644 --- a/cli/help.go +++ b/cli/help.go @@ -21,6 +21,7 @@ func HelpFunc(commands map[string]cli.CommandFactory) string { "write": struct{}{}, "server": struct{}{}, "status": struct{}{}, + "unwrap": struct{}{}, } // Determine the maximum key length, and classify based on type diff --git a/command/read.go b/command/read.go index 6cdde13d71..6e9c4d7ebf 100644 --- a/command/read.go +++ b/command/read.go @@ -3,8 +3,6 @@ package command import ( "flag" "fmt" - "os" - "reflect" "strings" "github.com/hashicorp/vault/api" @@ -63,23 +61,7 @@ func (c *ReadCommand) Run(args []string) int { // Handle single field output if field != "" { - if val, ok := secret.Data[field]; ok { - // c.Ui.Output() prints a CR character which in this case is - // not desired. Since Vault CLI currently only uses BasicUi, - // which writes to standard output, os.Stdout is used here to - // directly print the message. If mitchellh/cli exposes method - // to print without CR, this check needs to be removed. - if reflect.TypeOf(c.Ui).String() == "*cli.BasicUi" { - fmt.Fprintf(os.Stdout, fmt.Sprintf("%v", val)) - } else { - c.Ui.Output(fmt.Sprintf("%v", val)) - } - return 0 - } else { - c.Ui.Error(fmt.Sprintf( - "Field %s not present in secret", field)) - return 1 - } + return PrintRawField(c.Ui, secret, field) } return OutputSecret(c.Ui, format, secret) diff --git a/command/unwrap.go b/command/unwrap.go new file mode 100644 index 0000000000..1f6460a01b --- /dev/null +++ b/command/unwrap.go @@ -0,0 +1,133 @@ +package command + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "strings" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/meta" +) + +const ( + wrappedResponseLocation = "cubbyhole/response" +) + +// UnwrapCommand is a Command that behaves like ReadCommand but specifically +// for unwrapping cubbyhole-wrapped secrets +type UnwrapCommand struct { + meta.Meta +} + +func (c *UnwrapCommand) Run(args []string) int { + var format string + var field string + var err error + var secret *api.Secret + var flags *flag.FlagSet + flags = c.Meta.FlagSet("unwrap", meta.FlagSetDefault) + flags.StringVar(&format, "format", "table", "") + flags.StringVar(&field, "field", "", "") + flags.Usage = func() { c.Ui.Error(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + args = flags.Args() + if len(args) != 1 || len(args[0]) == 0 { + c.Ui.Error("Unwrap expects one argument: the ID of the wrapping token") + flags.Usage() + return 1 + } + + tokenID := args[0] + _, err = uuid.ParseUUID(tokenID) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Given token could not be parsed as a UUID: %s", err)) + return 1 + } + + client, err := c.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error initializing client: %s", err)) + return 2 + } + + client.SetToken(tokenID) + + secret, err = c.getUnwrappedResponse(client) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if secret == nil { + c.Ui.Error("Secret returned was nil") + return 1 + } + + // Handle single field output + if field != "" { + return PrintRawField(c.Ui, secret, field) + } + + return OutputSecret(c.Ui, format, secret) +} + +// getUnwrappedResponse is a helper to do the actual reading and unwrapping +func (c *UnwrapCommand) getUnwrappedResponse(client *api.Client) (*api.Secret, error) { + secret, err := client.Logical().Read(wrappedResponseLocation) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %s", wrappedResponseLocation, err) + } + if secret == nil { + return nil, fmt.Errorf("No value found at %s", wrappedResponseLocation) + } + if secret.Data == nil { + return nil, fmt.Errorf("\"data\" not found in wrapping response") + } + if _, ok := secret.Data["response"]; !ok { + return nil, fmt.Errorf("\"response\" not found in wrapping response \"data\" map") + } + + wrappedSecret := new(api.Secret) + buf := bytes.NewBufferString(secret.Data["response"].(string)) + dec := json.NewDecoder(buf) + dec.UseNumber() + if err := dec.Decode(wrappedSecret); err != nil { + return nil, fmt.Errorf("Error unmarshaling wrapped secret: %s", err) + } + + return wrappedSecret, nil +} + +func (c *UnwrapCommand) Synopsis() string { + return "Unwrap a wrapped secret" +} + +func (c *UnwrapCommand) Help() string { + helpText := ` +Usage: vault unwrap [options] + + Unwrap a wrapped secret. + + Unwraps the data wrapped by the given token ID. The returned result is the + same as a 'read' operation on a non-wrapped secret. + +General Options: +` + meta.GeneralOptionsUsage() + ` +Read Options: + + -format=table The format for output. By default it is a whitespace- + delimited table. This can also be json or yaml. + + -field=field If included, the raw value of the specified field + will be output raw to stdout. + +` + return strings.TrimSpace(helpText) +} diff --git a/command/util.go b/command/util.go index 1a409ce1a0..92a95aed1e 100644 --- a/command/util.go +++ b/command/util.go @@ -1,6 +1,14 @@ package command -import "github.com/hashicorp/vault/command/token" +import ( + "fmt" + "os" + "reflect" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/token" + "github.com/mitchellh/cli" +) // DefaultTokenHelper returns the token helper that is configured for Vault. func DefaultTokenHelper() (token.TokenHelper, error) { @@ -20,3 +28,39 @@ func DefaultTokenHelper() (token.TokenHelper, error) { } return &token.ExternalTokenHelper{BinaryPath: path}, nil } + +func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { + var val interface{} + switch field { + case "wrapping_token": + if secret.WrapInfo != nil { + val = secret.WrapInfo.Token + } + case "wrapping_token_ttl": + if secret.WrapInfo != nil { + val = secret.WrapInfo.TTL + } + case "refresh_interval": + val = secret.LeaseDuration + default: + val = secret.Data[field] + } + + if val != nil { + // c.Ui.Output() prints a CR character which in this case is + // not desired. Since Vault CLI currently only uses BasicUi, + // which writes to standard output, os.Stdout is used here to + // directly print the message. If mitchellh/cli exposes method + // to print without CR, this check needs to be removed. + if reflect.TypeOf(ui).String() == "*cli.BasicUi" { + fmt.Fprintf(os.Stdout, fmt.Sprintf("%v", val)) + } else { + ui.Output(fmt.Sprintf("%v", val)) + } + return 0 + } else { + ui.Error(fmt.Sprintf( + "Field %s not present in secret", field)) + return 1 + } +} diff --git a/vault/request_handling.go b/vault/request_handling.go index 1155d455b3..5a08922e17 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1,6 +1,7 @@ package vault import ( + "encoding/json" "sort" "strings" "time" @@ -391,12 +392,25 @@ func (c *Core) wrapInCubbyhole(req *logical.Request, resp *logical.Response) (*l httpResponse := logical.SanitizeResponse(resp) + // Because of the way that JSON encodes (likely just in Go) we actually get + // mixed-up values for ints if we simply put this object in the response + // and encode the whole thing; so instead we marshal it first, then store + // the string response. This actually ends up making it easier on the + // client side, too, as it becomes a straight read-string-pass-to-unmarshal + // operation. + + marshaledResponse, err := json.Marshal(httpResponse) + if err != nil { + c.logger.Printf("[ERR] core: failed to marshal wrapped response: %v", err) + return nil, ErrInternalError + } + cubbyReq := &logical.Request{ Operation: logical.CreateOperation, Path: "cubbyhole/response", ClientToken: te.ID, Data: map[string]interface{}{ - "response": httpResponse, + "response": string(marshaledResponse), }, }