From 5b3eaff55248b820b49c4aba1f52477f3003001b Mon Sep 17 00:00:00 2001 From: Conor Mongey Date: Mon, 6 Aug 2018 20:48:21 +0100 Subject: [PATCH] Support for reading v2 secrets --- vault/data_source_generic_secret.go | 13 +- vault/kv_helpers.go | 184 ++++++++++++++++++++++++++ website/docs/d/generic_secret.html.md | 3 + 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 vault/kv_helpers.go diff --git a/vault/data_source_generic_secret.go b/vault/data_source_generic_secret.go index 34f3537076..bb930abd35 100644 --- a/vault/data_source_generic_secret.go +++ b/vault/data_source_generic_secret.go @@ -22,6 +22,13 @@ func genericSecretDataSource() *schema.Resource { Description: "Full path from which a secret will be read.", }, + "version": { + Type: schema.TypeInt, + Required: false, + Optional: true, + Default: -1, + }, + "data_json": { Type: schema.TypeString, Computed: true, @@ -65,9 +72,11 @@ func genericSecretDataSourceRead(d *schema.ResourceData, meta interface{}) error client := meta.(*api.Client) path := d.Get("path").(string) + version := d.Get("version").(int) + + log.Printf("[DEBUG] Reading %s v%d from Vault", path, version) - log.Printf("[DEBUG] Reading %s from Vault", path) - secret, err := client.Logical().Read(path) + secret, err := versionedSecret(version, path, client) if err != nil { return fmt.Errorf("error reading from Vault: %s", err) } diff --git a/vault/kv_helpers.go b/vault/kv_helpers.go new file mode 100644 index 0000000000..e55f6ec91f --- /dev/null +++ b/vault/kv_helpers.go @@ -0,0 +1,184 @@ +package vault + +import ( + "fmt" + "io" + "path" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/strutil" +) + +func versionedSecret(requestedVersion int, path string, client *api.Client) (*api.Secret, error) { + mountPath, v2, err := isKVv2(path, client) + if err != nil { + return nil, err + } + + var versionParam map[string]string + + if v2 { + path = addPrefixToVKVPath(path, mountPath, "data") + if err != nil { + return nil, err + } + + if requestedVersion > 0 { + versionParam = map[string]string{ + "version": fmt.Sprintf("%d", requestedVersion), + } + } + } + + secret, err := kvReadRequest(client, path, versionParam) + + if err != nil { + return nil, err + } + + if v2 { + // This is a v2, grab the data field + if data, ok := secret.Data["data"]; ok && data != nil { + if dataMap, ok := data.(map[string]interface{}); ok { + secret.Data = dataMap + } + } + } + + return secret, nil +} + +func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) { + r := client.NewRequest("GET", "/v1/"+path) + for k, v := range params { + r.Params.Set(k, v) + } + resp, err := client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + secret, parseErr := api.ParseSecret(resp.Body) + switch parseErr { + case nil: + case io.EOF: + return nil, nil + default: + return nil, err + } + if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) { + return secret, nil + } + return nil, nil + } + if err != nil { + return nil, err + } + + return api.ParseSecret(resp.Body) +} + +func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { + // We don't want to use a wrapping call here so save any custom value and + // restore after + currentWrappingLookupFunc := client.CurrentWrappingLookupFunc() + client.SetWrappingLookupFunc(nil) + defer client.SetWrappingLookupFunc(currentWrappingLookupFunc) + + r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path) + resp, err := client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + // If we get a 404 we are using an older version of vault, default to + // version 1 + if resp != nil && resp.StatusCode == 404 { + return "", 1, nil + } + + return "", 0, err + } + + secret, err := api.ParseSecret(resp.Body) + if err != nil { + return "", 0, err + } + var mountPath string + if mountPathRaw, ok := secret.Data["path"]; ok { + mountPath = mountPathRaw.(string) + } + options := secret.Data["options"] + if options == nil { + return mountPath, 1, nil + } + versionRaw := options.(map[string]interface{})["version"] + if versionRaw == nil { + return mountPath, 1, nil + } + version := versionRaw.(string) + switch version { + case "", "1": + return mountPath, 1, nil + case "2": + return mountPath, 2, nil + } + + return mountPath, 1, nil +} + +func isKVv2(path string, client *api.Client) (string, bool, error) { + mountPath, version, err := kvPreflightVersionRequest(client, path) + if err != nil { + return "", false, err + } + + return mountPath, version == 2, nil +} + +func addPrefixToVKVPath(p, mountPath, apiPrefix string) string { + switch { + case p == mountPath, p == strings.TrimSuffix(mountPath, "/"): + return path.Join(mountPath, apiPrefix) + default: + p = strings.TrimPrefix(p, mountPath) + return path.Join(mountPath, apiPrefix, p) + } +} + +func getHeaderForMap(header string, data map[string]interface{}) string { + maxKey := 0 + for k := range data { + if len(k) > maxKey { + maxKey = len(k) + } + } + + // 4 for the column spaces and 5 for the len("value") + totalLen := maxKey + 4 + 5 + + equalSigns := totalLen - (len(header) + 2) + + // If we have zero or fewer equal signs bump it back up to two on either + // side of the header. + if equalSigns <= 0 { + equalSigns = 4 + } + + // If the number of equal signs is not divisible by two add a sign. + if equalSigns%2 != 0 { + equalSigns = equalSigns + 1 + } + + return fmt.Sprintf("%s %s %s", strings.Repeat("=", equalSigns/2), header, strings.Repeat("=", equalSigns/2)) +} + +func kvParseVersionsFlags(versions []string) []string { + versionsOut := make([]string, 0, len(versions)) + for _, v := range versions { + versionsOut = append(versionsOut, strutil.ParseStringSlice(v, ",")...) + } + + return versionsOut +} diff --git a/website/docs/d/generic_secret.html.md b/website/docs/d/generic_secret.html.md index c6ef7fecf8..144ac23485 100644 --- a/website/docs/d/generic_secret.html.md +++ b/website/docs/d/generic_secret.html.md @@ -47,6 +47,9 @@ default, this should be prefixed with `secret/`. Reading from other backends with this data source is possible; consult each backend's documentation to see which endpoints support the `GET` method. +* `version` - The version of the secret you want to read. Defaults to the +latest. Only supported when reading from a `v2` path. + ## Required Vault Capabilities Use of this resource requires the `read` capability on the given path.