From ce3205e8c4dadc996ea39661c8b60b779b8920ef Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Fri, 20 Apr 2018 12:59:08 -0700 Subject: [PATCH 01/14] Update kv command to use a preflight check --- command/command_test.go | 2 + command/kv_delete.go | 65 +++++++++--------- command/kv_destroy.go | 22 ++++-- command/kv_get.go | 30 +++++++-- command/kv_helpers.go | 122 ++++++++-------------------------- command/kv_list.go | 12 +++- command/kv_metadata_delete.go | 13 +++- command/kv_metadata_get.go | 12 +++- command/kv_metadata_put.go | 25 ++++--- command/kv_put.go | 39 +++++++---- command/kv_undelete.go | 24 +++++-- vault/capabilities.go | 6 +- vault/logical_system.go | 99 ++++++++++++++++++++++++++- vault/logical_system_test.go | 72 ++++++++++++++++++++ 14 files changed, 370 insertions(+), 173 deletions(-) diff --git a/command/command_test.go b/command/command_test.go index d91eb5c4e14f..77598a9a424c 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/hashicorp/go-hclog" + kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/builtin/logical/pki" @@ -41,6 +42,7 @@ var ( "pki": pki.Factory, "ssh": ssh.Factory, "transit": transit.Factory, + "kv": kv.Factory, } ) diff --git a/command/kv_delete.go b/command/kv_delete.go index 3f3fd0c1b910..5349b6dec44b 100644 --- a/command/kv_delete.go +++ b/command/kv_delete.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -87,13 +88,25 @@ func (c *KVDeleteCommand) Run(args []string) int { return 1 } + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + path := sanitizePath(args[0]) - var err error - if len(c.flagVersions) > 0 { - err = c.deleteVersions(path, kvParseVersionsFlags(c.flagVersions)) + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if v2 { + err = c.deleteV2(path, client) } else { - err = c.deleteLatest(path) + _, err = client.Logical().Delete(path) } + if err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 @@ -103,39 +116,29 @@ func (c *KVDeleteCommand) Run(args []string) int { return 0 } -func (c *KVDeleteCommand) deleteLatest(path string) error { +func (c *KVDeleteCommand) deleteV2(path string, client *api.Client) error { var err error - path, err = addPrefixToVKVPath(path, "data") - if err != nil { - return err - } - - client, err := c.Client() - if err != nil { - return err - } - - _, err = kvDeleteRequest(client, path) + switch { + case len(c.flagVersions) > 0: + path, err = addPrefixToVKVPath(path, "delete") + if err != nil { + return err + } - return err -} + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } -func (c *KVDeleteCommand) deleteVersions(path string, versions []string) error { - var err error - path, err = addPrefixToVKVPath(path, "delete") - if err != nil { - return err - } + _, err = client.Logical().Write(path, data) + default: - data := map[string]interface{}{ - "versions": versions, - } + path, err = addPrefixToVKVPath(path, "data") + if err != nil { + return err + } - client, err := c.Client() - if err != nil { - return err + _, err = client.Logical().Delete(path) } - _, err = kvWriteRequest(client, path, data) return err } diff --git a/command/kv_destroy.go b/command/kv_destroy.go index 25c6e98efecf..a24d7c872e25 100644 --- a/command/kv_destroy.go +++ b/command/kv_destroy.go @@ -86,23 +86,33 @@ func (c *KVDestroyCommand) Run(args []string) int { } var err error path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "destroy") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 } - data := map[string]interface{}{ - "versions": kvParseVersionsFlags(c.flagVersions), + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 } - - client, err := c.Client() + if !v2 { + c.UI.Error("Destroy not supported on KV Version 1") + return 1 + } + path, err = addPrefixToVKVPath(path, "destroy") if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/kv_get.go b/command/kv_get.go index 88819951b8b5..d93590c0c4bd 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -91,16 +91,25 @@ func (c *KVGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "data") + v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } var versionParam map[string]string - if c.flagVersion > 0 { - versionParam = map[string]string{ - "version": fmt.Sprintf("%d", c.flagVersion), + + if v2 { + path, err = addPrefixToVKVPath(path, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if c.flagVersion > 0 { + versionParam = map[string]string{ + "version": fmt.Sprintf("%d", c.flagVersion), + } } } @@ -128,8 +137,17 @@ func (c *KVGetCommand) Run(args []string) int { OutputData(c.UI, metadata) c.UI.Info("") } - if data, ok := secret.Data["data"]; ok && data != nil { - c.UI.Info(getHeaderForMap("Data", data.(map[string]interface{}))) + + data := secret.Data + if v2 && data != nil { + dataRaw := secret.Data["data"] + if dataRaw != nil { + data = dataRaw.(map[string]interface{}) + } + } + + if data != nil { + c.UI.Info(getHeaderForMap("Data", data)) OutputData(c.UI, data) } diff --git a/command/kv_helpers.go b/command/kv_helpers.go index 488d966adbc3..2d27862cfd25 100644 --- a/command/kv_helpers.go +++ b/command/kv_helpers.go @@ -4,22 +4,15 @@ import ( "errors" "fmt" "io" - "net/http" "path" "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" ) func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) { r := client.NewRequest("GET", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - for k, v := range params { r.Params.Set(k, v) } @@ -48,112 +41,53 @@ func kvReadRequest(client *api.Client, path string, params map[string]string) (* return api.ParseSecret(resp.Body) } -func kvListRequest(client *api.Client, path string) (*api.Secret, error) { - r := client.NewRequest("LIST", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} +func kvPreflightVersionRequest(client *api.Client, path string) (int, error) { + parts := strings.SplitN(path, "/", 2) + if len(parts) != 2 { + return 0, errors.New("invalid path") } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - // Set this for broader compatibility, but we use LIST above to be able to - // handle the wrapping lookup function - r.Method = "GET" - r.Params.Set("list", "true") + mountPath := parts[0] + + r := client.NewRequest("GET", "/v1/sys/internal/ui/mount/"+mountPath) 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 kvWriteRequest(client *api.Client, path string, data map[string]interface{}) (*api.Secret, error) { - r := client.NewRequest("PUT", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - if err := r.SetJSONBody(data); err != nil { - return nil, err + return 0, err } - resp, err := client.RawRequest(r) - if resp != nil { - defer resp.Body.Close() + secret, err := api.ParseSecret(resp.Body) + if err != nil { + return 0, err } - 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, err - } + options := secret.Data["options"] + if options == nil { + return 1, nil } - if err != nil { - return nil, err + versionRaw := options.(map[string]interface{})["version"] + if versionRaw == nil { + return 1, nil } - - if resp.StatusCode == 200 { - return api.ParseSecret(resp.Body) + version := versionRaw.(string) + switch version { + case "", "1": + return 1, nil + case "2": + return 2, nil } - return nil, nil + return 1, nil } -func kvDeleteRequest(client *api.Client, path string) (*api.Secret, error) { - r := client.NewRequest("DELETE", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - 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, err - } - } +func isKVv2(path string, client *api.Client) (bool, error) { + version, err := kvPreflightVersionRequest(client, path) if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - return api.ParseSecret(resp.Body) + return false, err } - return nil, nil + return version == 2, nil } func addPrefixToVKVPath(p, apiPrefix string) (string, error) { diff --git a/command/kv_list.go b/command/kv_list.go index 50613d506c74..b3f070fcf813 100644 --- a/command/kv_list.go +++ b/command/kv_list.go @@ -74,13 +74,21 @@ func (c *KVListCommand) Run(args []string) int { } path := ensureTrailingSlash(sanitizePath(args[0])) - path, err = addPrefixToVKVPath(path, "metadata") + v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvListRequest(client, path) + if v2 { + path, err = addPrefixToVKVPath(path, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + } + + secret, err := client.Logical().List(path) if err != nil { c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) return 2 diff --git a/command/kv_metadata_delete.go b/command/kv_metadata_delete.go index 8a764bb7bfda..fa8ea4607b89 100644 --- a/command/kv_metadata_delete.go +++ b/command/kv_metadata_delete.go @@ -71,13 +71,24 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int { } path := sanitizePath(args[0]) + + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 + } + path, err = addPrefixToVKVPath(path, "metadata") if err != nil { c.UI.Error(err.Error()) return 2 } - if _, err := kvDeleteRequest(client, path); err != nil { + if _, err := client.Logical().Delete(path); err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 } diff --git a/command/kv_metadata_get.go b/command/kv_metadata_get.go index 11855a275b3e..946901980326 100644 --- a/command/kv_metadata_get.go +++ b/command/kv_metadata_get.go @@ -75,13 +75,23 @@ func (c *KVMetadataGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 + } + path, err = addPrefixToVKVPath(path, "metadata") if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvReadRequest(client, path, nil) + secret, err := client.Logical().Read(path) if err != nil { c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) return 2 diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go index 95a6d6f5c30b..86d75bb0c97c 100644 --- a/command/kv_metadata_put.go +++ b/command/kv_metadata_put.go @@ -99,26 +99,35 @@ func (c *KVMetadataPutCommand) Run(args []string) int { return 1 } - var err error - path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "metadata") + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 } - data := map[string]interface{}{ - "max_versions": c.flagMaxVersions, - "cas_required": c.flagCASRequired, + path := sanitizePath(args[0]) + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 } - client, err := c.Client() + path, err = addPrefixToVKVPath(path, "metadata") if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + data := map[string]interface{}{ + "max_versions": c.flagMaxVersions, + "cas_required": c.flagCASRequired, + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/kv_put.go b/command/kv_put.go index ca981f885822..c020e4eba6b8 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -97,14 +97,19 @@ func (c *KVPutCommand) Run(args []string) int { stdin = c.testStdin } - if len(args) < 1 { + switch { + case len(args) < 1: c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 + case len(args) == 1: + c.UI.Error("Must supply data") + return 1 } var err error path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "data") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 @@ -116,22 +121,30 @@ func (c *KVPutCommand) Run(args []string) int { return 1 } - data = map[string]interface{}{ - "data": data, - "options": map[string]interface{}{}, - } - - if c.flagCAS > -1 { - data["options"].(map[string]interface{})["cas"] = c.flagCAS - } - - client, err := c.Client() + v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + if v2 { + path, err = addPrefixToVKVPath(path, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + data = map[string]interface{}{ + "data": data, + "options": map[string]interface{}{}, + } + + if c.flagCAS > -1 { + data["options"].(map[string]interface{})["cas"] = c.flagCAS + } + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/kv_undelete.go b/command/kv_undelete.go index 048d6f6885d5..bcc8d7c216c8 100644 --- a/command/kv_undelete.go +++ b/command/kv_undelete.go @@ -84,25 +84,35 @@ func (c *KVUndeleteCommand) Run(args []string) int { c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to undelete.") return 1 } - var err error - path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "undelete") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 } - data := map[string]interface{}{ - "versions": kvParseVersionsFlags(c.flagVersions), + path := sanitizePath(args[0]) + v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + if !v2 { + c.UI.Error("Undelete not supported on KV Version 1") + return 1 } - client, err := c.Client() + path, err = addPrefixToVKVPath(path, "undelete") if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/vault/capabilities.go b/vault/capabilities.go index db64347aafb4..d9990d79e77a 100644 --- a/vault/capabilities.go +++ b/vault/capabilities.go @@ -38,11 +38,15 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string, policies = append(policies, policy) } - _, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID) + entity, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID) if err != nil { return nil, err } + if entity != nil && entity.Disabled { + return nil, logical.ErrEntityDisabled + } + for _, item := range derivedPolicies { policy, err := c.policyStore.GetPolicy(ctx, item, PolicyTypeToken) if err != nil { diff --git a/vault/logical_system.go b/vault/logical_system.go index 7d873d8229b6..33208fa02541 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/vault/helper/compressutil" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/parseutil" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/wrapping" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -92,6 +93,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { "wrapping/pubkey", "replication/status", "internal/ui/mounts", + "internal/ui/mount/*", }, }, @@ -1075,6 +1077,20 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]), HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]), }, + &framework.Path{ + Pattern: "internal/ui/mount/(?P.+)", + Fields: map[string]*framework.FieldSchema{ + "path": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The path of the mount.", + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathInternalUIMountRead, + }, + HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]), + HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]), + }, }, } @@ -3395,9 +3411,6 @@ func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Reques } func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - b.Core.mountsLock.RLock() - defer b.Core.mountsLock.RUnlock() - resp := &logical.Response{ Data: make(map[string]interface{}), } @@ -3407,6 +3420,7 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic resp.Data["secret"] = secretMounts resp.Data["auth"] = authMounts + b.Core.mountsLock.RLock() for _, entry := range b.Core.mounts.Entries { if entry.Config.ListingVisibility == ListingVisibilityUnauth { info := map[string]interface{}{ @@ -3416,7 +3430,9 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic secretMounts[entry.Path] = info } } + b.Core.mountsLock.RUnlock() + b.Core.authLock.RLock() for _, entry := range b.Core.auth.Entries { if entry.Config.ListingVisibility == ListingVisibilityUnauth { info := map[string]interface{}{ @@ -3426,6 +3442,83 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic authMounts[entry.Path] = info } } + b.Core.authLock.RUnlock() + + return resp, nil +} + +func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + path := d.Get("path").(string) + if path == "" { + return logical.ErrorResponse("path not set"), logical.ErrInvalidRequest + } + path = sanitizeMountPath(path) + + me := b.Core.router.MatchingMountEntry(path) + if me == nil { + // Return a permission denied error here so this path cannot be used to + // brute force a list of mounts. + return nil, logical.ErrPermissionDenied + } + + resp := &logical.Response{ + Data: map[string]interface{}{ + "type": me.Type, + "description": me.Description, + "options": me.Options, + }, + } + + // If capabilities tells us we have access to the mount's path, go ahead and + // return the data. + capabilities, err := b.Core.Capabilities(ctx, req.ClientToken, path) + if err != nil { + return nil, err + } + if !strutil.StrListContains(capabilities, DenyCapability) { + return resp, nil + } + + // Load the ACL policies so we can walk the prefix for this mount + acl, _, _, err := b.Core.fetchACLTokenEntryAndEntity(req.ClientToken) + if err != nil { + return nil, err + } + + var aclCapabilitiesGiven bool + walkFn := func(s string, v interface{}) bool { + if v == nil { + return false + } + + perms := v.(*ACLPermissions) + + switch { + case perms.CapabilitiesBitmap&DenyCapabilityInt > 0: + return false + + case perms.CapabilitiesBitmap&CreateCapabilityInt > 0, + perms.CapabilitiesBitmap&DeleteCapabilityInt > 0, + perms.CapabilitiesBitmap&ListCapabilityInt > 0, + perms.CapabilitiesBitmap&ReadCapabilityInt > 0, + perms.CapabilitiesBitmap&SudoCapabilityInt > 0, + perms.CapabilitiesBitmap&UpdateCapabilityInt > 0: + + aclCapabilitiesGiven = true + return true + } + + return false + } + + acl.exactRules.WalkPrefix(path, walkFn) + if !aclCapabilitiesGiven { + acl.globRules.WalkPrefix(path, walkFn) + } + + if !aclCapabilitiesGiven { + return nil, logical.ErrPermissionDenied + } return resp, nil } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 129941a54b51..fef19ca3b380 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -2269,3 +2269,75 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) } } + +func TestSystemBackend_InternalUIMount(t *testing.T) { + core, b, rootToken := testCoreSystemBackend(t) + + req := logical.TestRequest(t, logical.UpdateOperation, "policy/secret") + req.ClientToken = rootToken + req.Data = map[string]interface{}{ + "rules": `path "secret/foo/*" { + capabilities = ["create", "read", "update", "delete", "list"] +}`, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + + req = logical.TestRequest(t, logical.UpdateOperation, "mounts/kv") + req.ClientToken = rootToken + req.Data = map[string]interface{}{ + "type": "kv", + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv") + req.ClientToken = rootToken + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "kv" { + t.Fatalf("Bad Response: %#v", resp) + } + + testMakeToken(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"}) + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != logical.ErrPermissionDenied { + t.Fatal("expected permission denied error") + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/secret") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "kv" { + t.Fatalf("Bad Response: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/sys") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "system" { + t.Fatalf("Bad Response: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/non-existent") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != logical.ErrPermissionDenied { + t.Fatal("expected permission denied error") + } +} From 58785364af1db2d551f8804b2ffba5c81bea4e6c Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Fri, 20 Apr 2018 17:17:08 -0700 Subject: [PATCH 02/14] Make the existing ui endpoint return the allowed mounts --- command/kv_delete.go | 10 +-- command/kv_destroy.go | 4 +- command/kv_get.go | 4 +- command/kv_helpers.go | 48 +++++------- command/kv_list.go | 4 +- command/kv_metadata_delete.go | 10 +-- command/kv_metadata_get.go | 9 +-- command/kv_metadata_put.go | 9 +-- command/kv_put.go | 9 +-- command/kv_undelete.go | 9 +-- vault/logical_system.go | 133 ++++++++++++++++++++++------------ vault/logical_system_test.go | 48 +++++++++++- 12 files changed, 172 insertions(+), 125 deletions(-) diff --git a/command/kv_delete.go b/command/kv_delete.go index 5349b6dec44b..9ce39976a9dd 100644 --- a/command/kv_delete.go +++ b/command/kv_delete.go @@ -95,14 +95,14 @@ func (c *KVDeleteCommand) Run(args []string) int { } path := sanitizePath(args[0]) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } if v2 { - err = c.deleteV2(path, client) + err = c.deleteV2(path, mountPath, client) } else { _, err = client.Logical().Delete(path) } @@ -116,11 +116,11 @@ func (c *KVDeleteCommand) Run(args []string) int { return 0 } -func (c *KVDeleteCommand) deleteV2(path string, client *api.Client) error { +func (c *KVDeleteCommand) deleteV2(path, mountPath string, client *api.Client) error { var err error switch { case len(c.flagVersions) > 0: - path, err = addPrefixToVKVPath(path, "delete") + path = addPrefixToVKVPath(path, mountPath, "delete") if err != nil { return err } @@ -132,7 +132,7 @@ func (c *KVDeleteCommand) deleteV2(path string, client *api.Client) error { _, err = client.Logical().Write(path, data) default: - path, err = addPrefixToVKVPath(path, "data") + path = addPrefixToVKVPath(path, mountPath, "data") if err != nil { return err } diff --git a/command/kv_destroy.go b/command/kv_destroy.go index a24d7c872e25..f622e3f05f00 100644 --- a/command/kv_destroy.go +++ b/command/kv_destroy.go @@ -93,7 +93,7 @@ func (c *KVDestroyCommand) Run(args []string) int { return 2 } - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -102,7 +102,7 @@ func (c *KVDestroyCommand) Run(args []string) int { c.UI.Error("Destroy not supported on KV Version 1") return 1 } - path, err = addPrefixToVKVPath(path, "destroy") + path = addPrefixToVKVPath(path, mountPath, "destroy") if err != nil { c.UI.Error(err.Error()) return 2 diff --git a/command/kv_get.go b/command/kv_get.go index d93590c0c4bd..aa42e6cc0106 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -91,7 +91,7 @@ func (c *KVGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -100,7 +100,7 @@ func (c *KVGetCommand) Run(args []string) int { var versionParam map[string]string if v2 { - path, err = addPrefixToVKVPath(path, "data") + path = addPrefixToVKVPath(path, mountPath, "data") if err != nil { c.UI.Error(err.Error()) return 2 diff --git a/command/kv_helpers.go b/command/kv_helpers.go index 2d27862cfd25..f65117eded57 100644 --- a/command/kv_helpers.go +++ b/command/kv_helpers.go @@ -1,7 +1,6 @@ package command import ( - "errors" "fmt" "io" "path" @@ -41,62 +40,55 @@ func kvReadRequest(client *api.Client, path string, params map[string]string) (* return api.ParseSecret(resp.Body) } -func kvPreflightVersionRequest(client *api.Client, path string) (int, error) { - parts := strings.SplitN(path, "/", 2) - if len(parts) != 2 { - return 0, errors.New("invalid path") - } - - mountPath := parts[0] - - r := client.NewRequest("GET", "/v1/sys/internal/ui/mount/"+mountPath) +func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { + r := client.NewRequest("GET", "/v1/sys/internal/ui/mount/"+path) resp, err := client.RawRequest(r) if resp != nil { defer resp.Body.Close() } if err != nil { - return 0, err + return "", 0, err } secret, err := api.ParseSecret(resp.Body) if err != nil { - return 0, err + return "", 0, err + } + var mountPath string + if mountPathRaw, ok := secret.Data["path"]; ok { + mountPath = mountPathRaw.(string) } options := secret.Data["options"] if options == nil { - return 1, nil + return mountPath, 1, nil } versionRaw := options.(map[string]interface{})["version"] if versionRaw == nil { - return 1, nil + return mountPath, 1, nil } version := versionRaw.(string) switch version { case "", "1": - return 1, nil + return mountPath, 1, nil case "2": - return 2, nil + return mountPath, 2, nil } - return 1, nil + return mountPath, 1, nil } -func isKVv2(path string, client *api.Client) (bool, error) { - version, err := kvPreflightVersionRequest(client, path) +func isKVv2(path string, client *api.Client) (string, bool, error) { + mountPath, version, err := kvPreflightVersionRequest(client, path) if err != nil { - return false, err + return "", false, err } - return version == 2, nil + return mountPath, version == 2, nil } -func addPrefixToVKVPath(p, apiPrefix string) (string, error) { - parts := strings.SplitN(p, "/", 2) - if len(parts) != 2 { - return "", errors.New("invalid path") - } - - return path.Join(parts[0], apiPrefix, parts[1]), nil +func addPrefixToVKVPath(p, mountPath, apiPrefix string) string { + p = strings.TrimPrefix(p, mountPath) + return path.Join(mountPath, apiPrefix, p) } func getHeaderForMap(header string, data map[string]interface{}) string { diff --git a/command/kv_list.go b/command/kv_list.go index b3f070fcf813..faedd6f3f429 100644 --- a/command/kv_list.go +++ b/command/kv_list.go @@ -74,14 +74,14 @@ func (c *KVListCommand) Run(args []string) int { } path := ensureTrailingSlash(sanitizePath(args[0])) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } if v2 { - path, err = addPrefixToVKVPath(path, "metadata") + path = addPrefixToVKVPath(path, mountPath, "metadata") if err != nil { c.UI.Error(err.Error()) return 2 diff --git a/command/kv_metadata_delete.go b/command/kv_metadata_delete.go index fa8ea4607b89..446533bdf307 100644 --- a/command/kv_metadata_delete.go +++ b/command/kv_metadata_delete.go @@ -71,8 +71,7 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int { } path := sanitizePath(args[0]) - - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -82,12 +81,7 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int { return 1 } - path, err = addPrefixToVKVPath(path, "metadata") - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - + path = addPrefixToVKVPath(path, mountPath, "metadata") if _, err := client.Logical().Delete(path); err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 diff --git a/command/kv_metadata_get.go b/command/kv_metadata_get.go index 946901980326..75c4cd22f7f8 100644 --- a/command/kv_metadata_get.go +++ b/command/kv_metadata_get.go @@ -75,7 +75,7 @@ func (c *KVMetadataGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -85,12 +85,7 @@ func (c *KVMetadataGetCommand) Run(args []string) int { return 1 } - path, err = addPrefixToVKVPath(path, "metadata") - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - + path = addPrefixToVKVPath(path, mountPath, "metadata") secret, err := client.Logical().Read(path) if err != nil { c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go index 86d75bb0c97c..32f8d9248152 100644 --- a/command/kv_metadata_put.go +++ b/command/kv_metadata_put.go @@ -106,7 +106,7 @@ func (c *KVMetadataPutCommand) Run(args []string) int { } path := sanitizePath(args[0]) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -116,12 +116,7 @@ func (c *KVMetadataPutCommand) Run(args []string) int { return 1 } - path, err = addPrefixToVKVPath(path, "metadata") - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - + path = addPrefixToVKVPath(path, mountPath, "metadata") data := map[string]interface{}{ "max_versions": c.flagMaxVersions, "cas_required": c.flagCASRequired, diff --git a/command/kv_put.go b/command/kv_put.go index c020e4eba6b8..e42b79dca749 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -121,19 +121,14 @@ func (c *KVPutCommand) Run(args []string) int { return 1 } - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } if v2 { - path, err = addPrefixToVKVPath(path, "data") - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - + path = addPrefixToVKVPath(path, mountPath, "data") data = map[string]interface{}{ "data": data, "options": map[string]interface{}{}, diff --git a/command/kv_undelete.go b/command/kv_undelete.go index bcc8d7c216c8..58eee93c0b9a 100644 --- a/command/kv_undelete.go +++ b/command/kv_undelete.go @@ -92,7 +92,7 @@ func (c *KVUndeleteCommand) Run(args []string) int { } path := sanitizePath(args[0]) - v2, err := isKVv2(path, client) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 @@ -102,12 +102,7 @@ func (c *KVUndeleteCommand) Run(args []string) int { return 1 } - path, err = addPrefixToVKVPath(path, "undelete") - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - + path = addPrefixToVKVPath(path, mountPath, "undelete") data := map[string]interface{}{ "versions": kvParseVersionsFlags(c.flagVersions), } diff --git a/vault/logical_system.go b/vault/logical_system.go index 33208fa02541..d3cb3e7616ba 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -12,6 +12,7 @@ import ( "hash" "net/http" "path/filepath" + "sort" "strconv" "strings" "sync" @@ -22,6 +23,7 @@ import ( uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/compressutil" "github.com/hashicorp/vault/helper/consts" + "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/parseutil" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/wrapping" @@ -3410,6 +3412,48 @@ func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Reques return resp, nil } +func hasMountAccess(acl *ACL, path string) bool { + // If an ealier policy is giving us access to the mount path then we can do + // a fast return. + capabilities := acl.Capabilities(path) + sort.Strings(capabilities) + if !strutil.StrListContains(capabilities, DenyCapability) { + return true + } + + var aclCapabilitiesGiven bool + walkFn := func(s string, v interface{}) bool { + if v == nil { + return false + } + + perms := v.(*ACLPermissions) + + switch { + case perms.CapabilitiesBitmap&DenyCapabilityInt > 0: + return false + + case perms.CapabilitiesBitmap&CreateCapabilityInt > 0, + perms.CapabilitiesBitmap&DeleteCapabilityInt > 0, + perms.CapabilitiesBitmap&ListCapabilityInt > 0, + perms.CapabilitiesBitmap&ReadCapabilityInt > 0, + perms.CapabilitiesBitmap&SudoCapabilityInt > 0, + perms.CapabilitiesBitmap&UpdateCapabilityInt > 0: + + aclCapabilitiesGiven = true + return true + } + + return false + } + acl.exactRules.WalkPrefix(path, walkFn) + if !aclCapabilitiesGiven { + acl.globRules.WalkPrefix(path, walkFn) + } + + return aclCapabilitiesGiven +} + func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { resp := &logical.Response{ Data: make(map[string]interface{}), @@ -3420,26 +3464,56 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic resp.Data["secret"] = secretMounts resp.Data["auth"] = authMounts + var acl *ACL + var isAuthed bool + var err error + if req.ClientToken != "" { + isAuthed = true + + var entity *identity.Entity + // Load the ACL policies so we can walk the prefix for this mount + acl, _, entity, err = b.Core.fetchACLTokenEntryAndEntity(req.ClientToken) + if err != nil { + return nil, err + } + if entity != nil && entity.Disabled { + return nil, logical.ErrEntityDisabled + } + + } + + hasAccess := func(me *MountEntry) bool { + if me.Config.ListingVisibility == ListingVisibilityUnauth { + return true + } + + if isAuthed { + return hasMountAccess(acl, me.Path) + } + + return false + } + b.Core.mountsLock.RLock() for _, entry := range b.Core.mounts.Entries { - if entry.Config.ListingVisibility == ListingVisibilityUnauth { - info := map[string]interface{}{ + if hasAccess(entry) { + secretMounts[entry.Path] = map[string]interface{}{ "type": entry.Type, "description": entry.Description, + "options": entry.Options, } - secretMounts[entry.Path] = info } } b.Core.mountsLock.RUnlock() b.Core.authLock.RLock() for _, entry := range b.Core.auth.Entries { - if entry.Config.ListingVisibility == ListingVisibilityUnauth { - info := map[string]interface{}{ + if hasAccess(entry) { + authMounts[entry.Path] = map[string]interface{}{ "type": entry.Type, "description": entry.Description, + "options": entry.Options, } - authMounts[entry.Path] = info } } b.Core.authLock.RUnlock() @@ -3463,60 +3537,23 @@ func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logica resp := &logical.Response{ Data: map[string]interface{}{ + "path": me.Path, "type": me.Type, "description": me.Description, "options": me.Options, }, } - // If capabilities tells us we have access to the mount's path, go ahead and - // return the data. - capabilities, err := b.Core.Capabilities(ctx, req.ClientToken, path) - if err != nil { - return nil, err - } - if !strutil.StrListContains(capabilities, DenyCapability) { - return resp, nil - } - // Load the ACL policies so we can walk the prefix for this mount - acl, _, _, err := b.Core.fetchACLTokenEntryAndEntity(req.ClientToken) + acl, _, entity, err := b.Core.fetchACLTokenEntryAndEntity(req.ClientToken) if err != nil { return nil, err } - - var aclCapabilitiesGiven bool - walkFn := func(s string, v interface{}) bool { - if v == nil { - return false - } - - perms := v.(*ACLPermissions) - - switch { - case perms.CapabilitiesBitmap&DenyCapabilityInt > 0: - return false - - case perms.CapabilitiesBitmap&CreateCapabilityInt > 0, - perms.CapabilitiesBitmap&DeleteCapabilityInt > 0, - perms.CapabilitiesBitmap&ListCapabilityInt > 0, - perms.CapabilitiesBitmap&ReadCapabilityInt > 0, - perms.CapabilitiesBitmap&SudoCapabilityInt > 0, - perms.CapabilitiesBitmap&UpdateCapabilityInt > 0: - - aclCapabilitiesGiven = true - return true - } - - return false - } - - acl.exactRules.WalkPrefix(path, walkFn) - if !aclCapabilitiesGiven { - acl.globRules.WalkPrefix(path, walkFn) + if entity != nil && entity.Disabled { + return nil, logical.ErrPermissionDenied } - if !aclCapabilitiesGiven { + if !hasMountAccess(acl, me.Path) { return nil, logical.ErrPermissionDenied } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index fef19ca3b380..7ae412658623 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -2218,7 +2218,7 @@ func TestSystemBackend_ToolsRandom(t *testing.T) { } func TestSystemBackend_InternalUIMounts(t *testing.T) { - b := testSystemBackend(t) + _, b, rootToken := testCoreSystemBackend(t) // Ensure no entries are in the endpoint as a starting point req := logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts") @@ -2235,6 +2235,48 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) } + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts") + req.ClientToken = rootToken + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("err: %v", err) + } + + exp = map[string]interface{}{ + "secret": map[string]interface{}{ + "secret/": map[string]interface{}{ + "type": "kv", + "description": "key/value secret storage", + "options": map[string]string{"version": "1"}, + }, + "cubbyhole/": map[string]interface{}{ + "type": "cubbyhole", + "description": "per-token private secret storage", + "options": map[string]string(nil), + }, + "identity/": map[string]interface{}{ + "options": map[string]string(nil), + "type": "identity", + "description": "identity store", + }, + "sys/": map[string]interface{}{ + "type": "system", + "description": "system endpoints used for control, policy and debugging", + "options": map[string]string(nil), + }, + }, + "auth": map[string]interface{}{ + "token/": map[string]interface{}{ + "type": "token", + "description": "token based credentials", + "options": map[string]string(nil), + }, + }, + } + if !reflect.DeepEqual(resp.Data, exp) { + t.Fatalf("got: %#v expect: %#v", resp.Data, exp) + } + // Mount-tune an auth mount req = logical.TestRequest(t, logical.UpdateOperation, "auth/token/tune") req.Data["listing_visibility"] = "unauth" @@ -2256,12 +2298,14 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "secret/": map[string]interface{}{ "type": "kv", "description": "key/value secret storage", + "options": map[string]string{"version": "1"}, }, }, "auth": map[string]interface{}{ "token/": map[string]interface{}{ "type": "token", "description": "token based credentials", + "options": map[string]string(nil), }, }, } @@ -2295,7 +2339,7 @@ func TestSystemBackend_InternalUIMount(t *testing.T) { t.Fatalf("Bad %#v %#v", err, resp) } - req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv") + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv/bar") req.ClientToken = rootToken resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { From 10a47235f19e93d8aaedde72e3ca773fa530321c Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Fri, 20 Apr 2018 18:04:43 -0700 Subject: [PATCH 03/14] Add kv subcommand tests --- command/kv_test.go | 427 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 command/kv_test.go diff --git a/command/kv_test.go b/command/kv_test.go new file mode 100644 index 000000000000..9e89e44162f0 --- /dev/null +++ b/command/kv_test.go @@ -0,0 +1,427 @@ +package command + +import ( + "io" + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testKVPutCommand(tb testing.TB) (*cli.MockUi, *KVPutCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVPutCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVPutCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "empty_kvs", + []string{"secret/write/foo"}, + "Must supply data", + 1, + }, + { + "kvs_no_value", + []string{"secret/write/foo", "foo"}, + "Failed to parse K=V data", + 1, + }, + { + "single_value", + []string{"secret/write/foo", "foo=bar"}, + "Success!", + 0, + }, + { + "multi_value", + []string{"secret/write/foo", "foo=bar", "zip=zap"}, + "Success!", + 0, + }, + { + "v2_single_value", + []string{"kv/write/foo", "foo=bar"}, + "created_time", + 0, + }, + { + "v2_multi_value", + []string{"kv/write/foo", "foo=bar", "zip=zap"}, + "created_time", + 0, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVPutCommand(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("v2_cas", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-cas", "0", "kv/write/cas", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "created_time") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, cmd = testKVPutCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-cas", "1", "kv/write/cas", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "created_time") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, cmd = testKVPutCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-cas", "1", "kv/write/cas", "bar=baz", + }) + if code != 2 { + t.Fatalf("expected 2 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "check-and-set parameter did not match the current version") { + t.Errorf("expected %q to contain %q", combined, "check-and-set parameter did not match the current version") + } + + }) + + t.Run("v1_data", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/write/data", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "Success!") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, rcmd := testReadCommand(t) + rcmd.client = client + code = rcmd.Run([]string{ + "secret/write/data", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if strings.Contains(combined, "data") { + t.Errorf("expected %q not to contain %q", combined, "data") + } + }) + + t.Run("stdin_full", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(`{"foo":"bar"}`)) + stdinW.Close() + }() + + _, cmd := testKVPutCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "secret/write/stdin_full", "-", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/stdin_full") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("stdin_value", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte("bar")) + stdinW.Close() + }() + + _, cmd := testKVPutCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "secret/write/stdin_value", "foo=-", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/stdin_value") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + _, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/write/integration", "foo=bar", "zip=zap", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/integration") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + if exp, act := "zap", secret.Data["zip"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVPutCommand(t) + assertNoTabs(t, cmd) + }) +} + +func testKVGetCommand(tb testing.TB) (*cli.MockUi, *KVGetCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVGetCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_found", + []string{"secret/nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/read/foo"}, + "foo", + 0, + }, + { + "v2_not_found", + []string{"kv/nope/not/once/never"}, + "", + 2, + }, + + { + "v2_read", + []string{"kv/read/foo"}, + "foo", + 0, + }, + { + "v2_read", + []string{"kv/read/foo"}, + "version", + 0, + }, + { + "v2_read_version", + []string{"--version", "1", "kv/read/foo"}, + "foo", + 0, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("kv/data/read/foo", map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVGetCommand(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("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVGetCommand(t) + assertNoTabs(t, cmd) + }) +} From eec334c3d27c2edb6ad8744e3bdc046567ede5f7 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 23 Apr 2018 15:29:23 -0400 Subject: [PATCH 04/14] Enable `-field` in `vault kv get/put` (#4426) * Enable `-field` in `vault kv get/put` Fixes #4424 * Unify nil value handling * Use preflight helper --- command/kv_get.go | 14 ++++++++++++-- command/kv_put.go | 6 +++++- command/util.go | 17 ++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/command/kv_get.go b/command/kv_get.go index aa42e6cc0106..c66e36fdab59 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -43,7 +43,7 @@ Usage: vault kv get [options] KEY } func (c *KVGetCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) // Common Options f := set.NewFlagSet("Common Options") @@ -124,7 +124,17 @@ func (c *KVGetCommand) Run(args []string) int { } if c.flagField != "" { - return PrintRawField(c.UI, secret, c.flagField) + if v2 { + // This is a v2, pass in the data field + if data, ok := secret.Data["data"]; ok && data != nil { + return PrintRawField(c.UI, data, c.flagField) + } else { + c.UI.Error(fmt.Sprintf("No data found at %s", path)) + return 2 + } + } else { + return PrintRawField(c.UI, secret, c.flagField) + } } // If we have wrap info print the secret normally. diff --git a/command/kv_put.go b/command/kv_put.go index e42b79dca749..56fac3e0761c 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -55,7 +55,7 @@ Usage: vault kv put [options] KEY [DATA] } func (c *KVPutCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) // Common Options f := set.NewFlagSet("Common Options") @@ -152,5 +152,9 @@ func (c *KVPutCommand) Run(args []string) int { return 0 } + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + return OutputSecret(c.UI, secret) } diff --git a/command/util.go b/command/util.go index b418ce7fc8ec..d9718ffc8746 100644 --- a/command/util.go +++ b/command/util.go @@ -20,7 +20,7 @@ func DefaultTokenHelper() (token.TokenHelper, error) { // RawField extracts the raw field from the given data and returns it as a // string for printing purposes. -func RawField(secret *api.Secret, field string) (interface{}, bool) { +func RawField(secret *api.Secret, field string) interface{} { var val interface{} switch { case secret.Auth != nil: @@ -72,13 +72,20 @@ func RawField(secret *api.Secret, field string) (interface{}, bool) { } } - return val, val != nil + return val } // PrintRawField prints raw field from the secret. -func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { - val, ok := RawField(secret, field) - if !ok { +func PrintRawField(ui cli.Ui, data interface{}, field string) int { + var val interface{} + switch data.(type) { + case *api.Secret: + val = RawField(data.(*api.Secret), field) + case map[string]interface{}: + val = data.(map[string]interface{})[field] + } + + if val == nil { ui.Error(fmt.Sprintf("Field %q not present in secret", field)) return 1 } From 42da68170b7509817e31416f4c46d33dc1aa0fd5 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 23 Apr 2018 15:33:37 -0400 Subject: [PATCH 05/14] Update vkv plugin --- .../vault-plugin-secrets-kv/backend.go | 42 ++++- .../vault-plugin-secrets-kv/passthrough.go | 10 +- .../passthrough_downgrader.go | 158 ------------------ .../vault-plugin-secrets-kv/path_config.go | 8 +- .../vault-plugin-secrets-kv/path_metadata.go | 1 + .../vault-plugin-secrets-kv/types.proto | 1 + vendor/vendor.json | 6 +- 7 files changed, 49 insertions(+), 177 deletions(-) delete mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go index 3aa4542df414..aa81126de32a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go @@ -60,6 +60,10 @@ type versionedKVBackend struct { // upgrading is an atomic value denoting if the backend is in the process of // upgrading its data. upgrading *uint32 + + // globalConfig is a cached value for fast lookup + globalConfig *Configuration + globalConfigLock *sync.RWMutex } // Factory will return a logical backend of type versionedKVBackend or @@ -85,7 +89,8 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, // Factory returns a new backend as logical.Backend. func VersionedKVFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { b := &versionedKVBackend{ - upgrading: new(uint32), + upgrading: new(uint32), + globalConfigLock: new(sync.RWMutex), } if conf.BackendUUID == "" { return nil, errors.New("could not initialize versioned K/V Store, no UUID was provided") @@ -207,6 +212,10 @@ func (b *versionedKVBackend) Invalidate(ctx context.Context, key string) { b.l.Lock() b.keyEncryptedWrapper = nil b.l.Unlock() + case path.Join(b.storagePrefix, configPath): + b.globalConfigLock.Lock() + b.globalConfig = nil + b.globalConfigLock.Unlock() } } @@ -301,19 +310,40 @@ func (b *versionedKVBackend) getKeyEncryptor(ctx context.Context, s logical.Stor // config takes a storage object and returns a configuration object func (b *versionedKVBackend) config(ctx context.Context, s logical.Storage) (*Configuration, error) { + b.globalConfigLock.RLock() + if b.globalConfig != nil { + defer b.globalConfigLock.RUnlock() + return &Configuration{ + CasRequired: b.globalConfig.CasRequired, + MaxVersions: b.globalConfig.MaxVersions, + }, nil + } + + b.globalConfigLock.RUnlock() + b.globalConfigLock.Lock() + defer b.globalConfigLock.Unlock() + + // Verify this hasn't already changed + if b.globalConfig != nil { + return &Configuration{ + CasRequired: b.globalConfig.CasRequired, + MaxVersions: b.globalConfig.MaxVersions, + }, nil + } + raw, err := s.Get(ctx, path.Join(b.storagePrefix, configPath)) if err != nil { return nil, err } conf := &Configuration{} - if raw == nil { - return conf, nil + if raw != nil { + if err := proto.Unmarshal(raw.Value, conf); err != nil { + return nil, err + } } - if err := proto.Unmarshal(raw.Value, conf); err != nil { - return nil, err - } + b.globalConfig = conf return conf, nil } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go index b6a9f03513b8..036e351e7c78 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go @@ -36,14 +36,10 @@ func LeasedPassthroughBackendFactory(ctx context.Context, conf *logical.BackendC // LeaseSwitchedPassthroughBackend returns a PassthroughBackend // with leases switched on or off func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendConfig, leases bool) (logical.Backend, error) { - passthroughBackend := &PassthroughBackend{ + b := &PassthroughBackend{ generateLeases: leases, } - var b Passthrough = &PassthroughDowngrader{ - next: passthroughBackend, - } - backend := &framework.Backend{ BackendType: logical.TypeLogical, Help: strings.TrimSpace(passthroughHelp), @@ -89,9 +85,9 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC return nil, fmt.Errorf("Configuation passed into backend is nil") } backend.Setup(ctx, conf) - passthroughBackend.Backend = backend + b.Backend = backend - return passthroughBackend, nil + return b, nil } // PassthroughBackend is used storing secrets directly into the physical diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go deleted file mode 100644 index 2f7096f87bec..000000000000 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go +++ /dev/null @@ -1,158 +0,0 @@ -package kv - -import ( - "context" - "net/http" - "strings" - - "github.com/hashicorp/vault/helper/consts" - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/framework" -) - -// PassthroughDowngrader wraps a normal passthrough backend and downgrades the -// request object from the newer Versioned API to the older Passthrough API. -// This allows us to use the new "vault kv" subcommand with a non-versioned -// instance of the kv store without doing a preflight API version check. The -// CLI will always use the new API definition and this object will make it -// compatible with the passthrough backend. The "X-Vault-Kv-Client" header is -// used to know the request originated from the CLI and uses the newer API. -type PassthroughDowngrader struct { - next Passthrough -} - -func (b *PassthroughDowngrader) handleExistenceCheck() framework.ExistenceFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { - if !b.shouldDowngrade(req) { - return b.next.handleExistenceCheck()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return false, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - return b.next.handleExistenceCheck()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleRead() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleRead()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - if _, ok := data.Raw["version"]; ok { - return logical.ErrorResponse("retrieving a version is not supported when versioning is disabled"), logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - resp, err := b.next.handleRead()(ctx, reqDown, data) - if resp != nil && resp.Data != nil { - resp.Data = map[string]interface{}{ - "data": resp.Data, - "metadata": nil, - } - } - - return resp, err - } -} - -func (b *PassthroughDowngrader) handleWrite() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleWrite()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - // Validate the data map is what we expect - switch req.Data["data"].(type) { - case map[string]interface{}: - default: - return logical.ErrorResponse("could not downgrade request, unexpected data format"), logical.ErrInvalidRequest - } - - // Move the data object up a level and ignore the options object. - reqDown.Data = req.Data["data"].(map[string]interface{}) - - return b.next.handleWrite()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleDelete() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleDelete()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - return b.next.handleDelete()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleList() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleList()(ctx, req, data) - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "metadata/") - - return b.next.handleList()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) shouldDowngrade(req *logical.Request) bool { - return http.Header(req.Headers).Get(consts.VaultKVCLIClientHeader) != "" -} - -// invalidPaths returns an error if we are trying to access an versioned only -// path on a non-versioned kv store. -func (b *PassthroughDowngrader) invalidPath(req *logical.Request) *logical.Response { - switch { - case req.Path == "config": - fallthrough - case strings.HasPrefix(req.Path, "metadata/"): - fallthrough - case strings.HasPrefix(req.Path, "archive/"): - fallthrough - case strings.HasPrefix(req.Path, "unarchive/"): - fallthrough - case strings.HasPrefix(req.Path, "destroy/"): - return logical.ErrorResponse("path is not supported when versioning is disabled") - } - - return nil -} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go index 80946a4119ec..6c9aa974151c 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go @@ -42,9 +42,6 @@ func (b *versionedKVBackend) pathConfigRead() framework.OperationFunc { if err != nil { return nil, err } - if config == nil { - return nil, nil - } return &logical.Response{ Data: map[string]interface{}{ @@ -91,6 +88,11 @@ func (b *versionedKVBackend) pathConfigWrite() framework.OperationFunc { return nil, err } + b.globalConfigLock.Lock() + defer b.globalConfigLock.Unlock() + + b.globalConfig = config + return nil, nil } } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go index 4a9706c45543..6e2a88c978b0 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go @@ -105,6 +105,7 @@ func (b *versionedKVBackend) pathMetadataRead() framework.OperationFunc { "created_time": ptypesTimestampToString(meta.CreatedTime), "updated_time": ptypesTimestampToString(meta.UpdatedTime), "max_versions": meta.MaxVersions, + "cas_required": meta.CasRequired, }, }, nil } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto index 275d7816b128..aaa3a821e14b 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto @@ -3,6 +3,7 @@ package kv; import "google/protobuf/timestamp.proto"; +// If values are added to this, be sure to update the config() function message Configuration { uint32 max_versions = 1; bool cas_required = 2; diff --git a/vendor/vendor.json b/vendor/vendor.json index e5df1e5b6e12..8924b4aaeb1b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1339,10 +1339,10 @@ "revisionTime": "2018-04-23T14:10:30Z" }, { - "checksumSHA1": "m3cQgQrCSuWHiPA339FaZU6LuHU=", + "checksumSHA1": "mawUYCTqiIcegkYTCG9fZChK4kQ=", "path": "github.com/hashicorp/vault-plugin-secrets-kv", - "revision": "bc6216eebacf73fab61fd5cc7535b5eda7a74c98", - "revisionTime": "2018-04-09T21:22:48Z" + "revision": "d5a07c3d99f7fa02dd23d6dbff98d24e0eedf06b", + "revisionTime": "2018-04-23T19:31:27Z" }, { "checksumSHA1": "vTfeYxi0Z1y176bjQaYh1/FpQ9s=", From 546246224b36f574e5ccaaab2ccc833fc7eb6aa5 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 13:07:17 -0700 Subject: [PATCH 06/14] Add all the mount info when authenticated --- vault/logical_system.go | 101 ++++++++++++++++++++--------------- vault/logical_system_test.go | 71 +++++++++++++++++++----- 2 files changed, 116 insertions(+), 56 deletions(-) diff --git a/vault/logical_system.go b/vault/logical_system.go index b522aa165c75..f8679325209d 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -1538,6 +1538,41 @@ func (b *SystemBackend) handleRekeyDeleteRecovery(ctx context.Context, req *logi return b.handleRekeyDelete(ctx, req, data, true) } +func mountInfo(entry *MountEntry) map[string]interface{} { + info := map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "accessor": entry.Accessor, + "local": entry.Local, + "seal_wrap": entry.SealWrap, + "options": entry.Options, + } + entryConfig := map[string]interface{}{ + "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), + "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), + "force_no_cache": entry.Config.ForceNoCache, + "plugin_name": entry.Config.PluginName, + } + if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { + entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) + } + if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { + entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string) + } + // Even though empty value is valid for ListingVisibility, we can ignore + // this case during mount since there's nothing to unset/hide. + if len(entry.Config.ListingVisibility) > 0 { + entryConfig["listing_visibility"] = entry.Config.ListingVisibility + } + if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + entryConfig["passthrough_request_headers"] = rawVal.([]string) + } + + info["config"] = entryConfig + + return info +} + // handleMountTable handles the "mounts" endpoint to provide the mount table func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.Core.mountsLock.RLock() @@ -1549,36 +1584,7 @@ func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Reque for _, entry := range b.Core.mounts.Entries { // Populate mount info - info := map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, - "accessor": entry.Accessor, - "local": entry.Local, - "seal_wrap": entry.SealWrap, - "options": entry.Options, - } - entryConfig := map[string]interface{}{ - "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), - "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), - "force_no_cache": entry.Config.ForceNoCache, - "plugin_name": entry.Config.PluginName, - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { - entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { - entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string) - } - // Even though empty value is valid for ListingVisibility, we can ignore - // this case during mount since there's nothing to unset/hide. - if len(entry.Config.ListingVisibility) > 0 { - entryConfig["listing_visibility"] = entry.Config.ListingVisibility - } - if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { - entryConfig["passthrough_request_headers"] = rawVal.([]string) - } - - info["config"] = entryConfig + info := mountInfo(entry) resp.Data[entry.Path] = info } @@ -3454,6 +3460,7 @@ func hasMountAccess(acl *ACL, path string) bool { return false } + acl.exactRules.WalkPrefix(path, walkFn) if !aclCapabilitiesGiven { acl.globRules.WalkPrefix(path, walkFn) @@ -3505,10 +3512,15 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic b.Core.mountsLock.RLock() for _, entry := range b.Core.mounts.Entries { if hasAccess(entry) { - secretMounts[entry.Path] = map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, - "options": entry.Options, + if isAuthed { + // If this is an authed request return all the mount info + secretMounts[entry.Path] = mountInfo(entry) + } else { + secretMounts[entry.Path] = map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "options": entry.Options, + } } } } @@ -3517,10 +3529,15 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic b.Core.authLock.RLock() for _, entry := range b.Core.auth.Entries { if hasAccess(entry) { - authMounts[entry.Path] = map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, - "options": entry.Options, + if isAuthed { + // If this is an authed request return all the mount info + authMounts[entry.Path] = mountInfo(entry) + } else { + authMounts[entry.Path] = map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "options": entry.Options, + } } } } @@ -3544,13 +3561,9 @@ func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logica } resp := &logical.Response{ - Data: map[string]interface{}{ - "path": me.Path, - "type": me.Type, - "description": me.Description, - "options": me.Options, - }, + Data: mountInfo(me), } + resp.Data["path"] = me.Path // Load the ACL policies so we can walk the prefix for this mount acl, _, entity, err := b.Core.fetchACLTokenEntryAndEntity(req) diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 7ae412658623..07dfb834c3e9 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -2247,34 +2247,81 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "secret/": map[string]interface{}{ "type": "kv", "description": "key/value secret storage", - "options": map[string]string{"version": "1"}, + "accessor": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string{ + "version": "1", + }, + }, + "sys/": map[string]interface{}{ + "type": "system", + "description": "system endpoints used for control, policy and debugging", + "accessor": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), }, "cubbyhole/": map[string]interface{}{ - "type": "cubbyhole", "description": "per-token private secret storage", - "options": map[string]string(nil), + "type": "cubbyhole", + "accessor": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": true, + "seal_wrap": false, + "options": map[string]string(nil), }, "identity/": map[string]interface{}{ - "options": map[string]string(nil), - "type": "identity", "description": "identity store", - }, - "sys/": map[string]interface{}{ - "type": "system", - "description": "system endpoints used for control, policy and debugging", - "options": map[string]string(nil), + "type": "identity", + "accessor": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), }, }, "auth": map[string]interface{}{ "token/": map[string]interface{}{ + "options": map[string]string(nil), + "config": map[string]interface{}{ + "default_lease_ttl": int64(0), + "max_lease_ttl": int64(0), + "force_no_cache": false, + "plugin_name": "", + }, "type": "token", "description": "token based credentials", - "options": map[string]string(nil), + "accessor": resp.Data["auth"].(map[string]interface{})["token/"].(map[string]interface{})["accessor"], + "local": false, + "seal_wrap": false, }, }, } if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("got: %#v expect: %#v", resp.Data, exp) + t.Fatalf("got: %#v \n\n expect: %#v", resp.Data, exp) } // Mount-tune an auth mount From 16be302b48597408e363050880006d1fe051e856 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 13:08:35 -0700 Subject: [PATCH 07/14] Add fix the error message on put --- command/kv_put.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/kv_put.go b/command/kv_put.go index 56fac3e0761c..a4524f9c8944 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -99,7 +99,7 @@ func (c *KVPutCommand) Run(args []string) int { switch { case len(args) < 1: - c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + c.UI.Error(fmt.Sprintf("Not enough arguments (expected >1, got %d)", len(args))) return 1 case len(args) == 1: c.UI.Error("Must supply data") From a3eb2121c519942fc5415b0bdf44cfbc75c600eb Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 13:31:45 -0700 Subject: [PATCH 08/14] add metadata test --- command/kv_get.go | 1 + command/kv_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/command/kv_get.go b/command/kv_get.go index c66e36fdab59..80f272ddde25 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -150,6 +150,7 @@ func (c *KVGetCommand) Run(args []string) int { data := secret.Data if v2 && data != nil { + data = nil dataRaw := secret.Data["data"] if dataRaw != nil { data = dataRaw.(map[string]interface{}) diff --git a/command/kv_test.go b/command/kv_test.go index 9e89e44162f0..697a105bb6cf 100644 --- a/command/kv_test.go +++ b/command/kv_test.go @@ -344,6 +344,19 @@ func TestKVGetCommand(t *testing.T) { "foo", 0, }, + { + "v1_field", + []string{"-field", "foo", "secret/read/foo"}, + "bar", + 0, + }, + { + "v2_field", + []string{"-field", "foo", "kv/read/foo"}, + "bar", + 0, + }, + { "v2_not_found", []string{"kv/nope/not/once/never"}, @@ -425,3 +438,92 @@ func TestKVGetCommand(t *testing.T) { assertNoTabs(t, cmd) }) } + +func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVMetadataGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVMetadataGetCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "v1", + []string{"secret/foo"}, + "Metadata not supported on KV Version 1", + 1, + }, + { + "metadata_exists", + []string{"kv/foo"}, + "current_version", + 0, + }, + { + "versions_exist", + []string{"kv/foo"}, + "deletion_time", + 0, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("kv/data/foo", map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVMetadataGetCommand(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("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVMetadataGetCommand(t) + assertNoTabs(t, cmd) + }) +} From b6cc7477b4a6d53e921fb840a90bb3e198bb6068 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 13:36:43 -0700 Subject: [PATCH 09/14] No need to sort the capabilities --- vault/logical_system.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/vault/logical_system.go b/vault/logical_system.go index f8679325209d..7b4919079718 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -12,7 +12,6 @@ import ( "hash" "net/http" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -3430,7 +3429,6 @@ func hasMountAccess(acl *ACL, path string) bool { // If an ealier policy is giving us access to the mount path then we can do // a fast return. capabilities := acl.Capabilities(path) - sort.Strings(capabilities) if !strutil.StrListContains(capabilities, DenyCapability) { return true } From e13c62d441a9fcd54c53fa4efea6301fcfa5a318 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 13:40:57 -0700 Subject: [PATCH 10/14] Remove the kv client header --- helper/consts/consts.go | 2 -- vault/cors.go | 2 -- vault/router.go | 21 --------------------- 3 files changed, 25 deletions(-) diff --git a/helper/consts/consts.go b/helper/consts/consts.go index 6c9558aa673e..eee59d9c999d 100644 --- a/helper/consts/consts.go +++ b/helper/consts/consts.go @@ -4,6 +4,4 @@ const ( // ExpirationRestoreWorkerCount specifies the number of workers to use while // restoring leases into the expiration manager ExpirationRestoreWorkerCount = 64 - - VaultKVCLIClientHeader = "X-Vault-Kv-Client" ) diff --git a/vault/cors.go b/vault/cors.go index 4ec8f8ff9873..6b0920a73b6b 100644 --- a/vault/cors.go +++ b/vault/cors.go @@ -7,7 +7,6 @@ import ( "sync/atomic" "github.com/hashicorp/errwrap" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) @@ -27,7 +26,6 @@ var StdAllowedHeaders = []string{ "X-Vault-Wrap-Format", "X-Vault-Wrap-TTL", "X-Vault-Policy-Override", - consts.VaultKVCLIClientHeader, } // CORSConfig stores the state of the CORS configuration. diff --git a/vault/router.go b/vault/router.go index 32b036824975..d79cc16338bc 100644 --- a/vault/router.go +++ b/vault/router.go @@ -10,17 +10,10 @@ import ( "github.com/armon/go-metrics" "github.com/armon/go-radix" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/logical" ) -var ( - whitelistedHeaders = []string{ - consts.VaultKVCLIClientHeader, - } -) - // Router is used to do prefix based routing of a request to a logical backend type Router struct { l sync.RWMutex @@ -639,20 +632,6 @@ func pathsToRadix(paths []string) *radix.Tree { func filteredPassthroughHeaders(origHeaders map[string][]string, passthroughHeaders []string) map[string][]string { retHeaders := make(map[string][]string) - // Handle whitelisted values - for _, header := range whitelistedHeaders { - if val, ok := origHeaders[header]; ok { - retHeaders[header] = val - } else { - // Try to check if a lowercased version of the header exists in the - // originating request. The header key that gets used is the one from the - // whitelist. - if val, ok := origHeaders[strings.ToLower(header)]; ok { - retHeaders[header] = val - } - } - } - // Short-circuit if there's nothing to filter if len(passthroughHeaders) == 0 { return retHeaders From 534a8e820cfad4b23c786f172d6500923483fb22 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 23 Apr 2018 17:00:57 -0400 Subject: [PATCH 11/14] kv patch command (#4432) --- command/commands.go | 7 ++ command/kv_patch.go | 195 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 command/kv_patch.go diff --git a/command/commands.go b/command/commands.go index eb3677c9917d..7b9e4a60e58c 100644 --- a/command/commands.go +++ b/command/commands.go @@ -695,6 +695,13 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, }, nil }, + "kv patch": func() (cli.Command, error) { + return &KVPatchCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, "kv get": func() (cli.Command, error) { return &KVGetCommand{ BaseCommand: &BaseCommand{ diff --git a/command/kv_patch.go b/command/kv_patch.go new file mode 100644 index 000000000000..bbfc164e7e54 --- /dev/null +++ b/command/kv_patch.go @@ -0,0 +1,195 @@ +package command + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVPatchCommand)(nil) +var _ cli.CommandAutocomplete = (*KVPatchCommand)(nil) + +type KVPatchCommand struct { + *BaseCommand + + testStdin io.Reader // for tests +} + +func (c *KVPatchCommand) Synopsis() string { + return "Sets or updates data in the KV store without overwriting." +} + +func (c *KVPatchCommand) Help() string { + helpText := ` +Usage: vault kv put [options] KEY [DATA] + + *NOTE*: This is only supported for KV v2 engine mounts. + + Writes the data to the given path in the key-value store. The data can be of + any type. + + $ vault kv patch secret/foo bar=baz + + The data can also be consumed from a file on disk by prefixing with the "@" + symbol. For example: + + $ vault kv patch secret/foo @data.json + + Or it can be read from stdin using the "-" symbol: + + $ echo "abcd1234" | vault kv patch secret/foo bar=- + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVPatchCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + return set +} + +func (c *KVPatchCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVPatchCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVPatchCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected >1, got %d)", len(args))) + return 1 + case len(args) == 1: + c.UI.Error("Must supply data") + return 1 + } + + var err error + path := sanitizePath(args[0]) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + newData, err := parseArgsData(stdin, args[1:]) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) + return 1 + } + + mountPath, v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if !v2 { + c.UI.Error(fmt.Sprintf("K/V engine mount must be version 2 for patch support")) + return 2 + } + + path = addPrefixToVKVPath(path, mountPath, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // First, do a read + secret, err := kvReadRequest(client, path, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err)) + return 2 + } + + // Make sure a value already exists + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + // Verify metadata found + rawMeta, ok := secret.Data["metadata"] + if !ok || rawMeta == nil { + c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path)) + return 2 + } + meta, ok := rawMeta.(map[string]interface{}) + if !ok { + c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path)) + return 2 + } + if meta == nil { + c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path)) + return 2 + } + + // Verify old data found + rawData, ok := secret.Data["data"] + if !ok || rawData == nil { + c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path)) + return 2 + } + data, ok := rawData.(map[string]interface{}) + if !ok { + c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path)) + return 2 + } + if data == nil { + c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path)) + return 2 + } + + // Copy new data over + for k, v := range newData { + data[k] = v + } + + secret, err = client.Logical().Write(path, map[string]interface{}{ + "data": data, + "options": map[string]interface{}{ + "cas": meta["version"], + }, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) + return 2 + } + if secret == nil { + // Don't output anything unless using the "table" format + if Format(c.UI) == "table" { + c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path)) + } + return 0 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, secret) +} From 83710663aaada4382bd6043bc9139dd53ea00a80 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 14:06:07 -0700 Subject: [PATCH 12/14] Fix test --- http/sys_internal_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/http/sys_internal_test.go b/http/sys_internal_test.go index dc6c5ac44fce..d3c066f70c84 100644 --- a/http/sys_internal_test.go +++ b/http/sys_internal_test.go @@ -65,12 +65,14 @@ func TestSysInternal_UIMounts(t *testing.T) { "secret/": map[string]interface{}{ "type": "kv", "description": "key/value secret storage", + "options": map[string]interface{}{"version": "1"}, }, }, "auth": map[string]interface{}{ "token/": map[string]interface{}{ "type": "token", "description": "token based credentials", + "options": interface{}(nil), }, }, }, From b63d799f8640c54cb0f77081a861ffbba90804d9 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Mon, 23 Apr 2018 14:33:25 -0700 Subject: [PATCH 13/14] Fix tests --- vault/core_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/vault/core_test.go b/vault/core_test.go index b63ba822a43d..bd3591d3d10c 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -2314,7 +2314,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) { Path: "foo/test", ClientToken: root, Headers: map[string][]string{ - "X-Vault-Kv-Client": []string{"foo"}, "Should-Passthrough": []string{"foo"}, "Should-Passthrough-Case-Insensitive": []string{"baz"}, "Should-Not-Passthrough": []string{"bar"}, @@ -2328,16 +2327,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) { // Check the headers headers := noop.Requests[0].Headers - // Test whitelisted values - if val, ok := headers["X-Vault-Kv-Client"]; ok { - expected := []string{"foo"} - if !reflect.DeepEqual(val, expected) { - t.Fatalf("expected: %v, got: %v", expected, val) - } - } else { - t.Fatalf("expected 'X-Vault-Kv-Client' to be present in the headers map") - } - // Test passthrough values if val, ok := headers["Should-Passthrough"]; ok { expected := []string{"foo"} From de5b40c3cda755bffef6018be85561691fda71ef Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 23 Apr 2018 17:40:19 -0400 Subject: [PATCH 14/14] Use permission denied instead of entity disabled --- vault/capabilities.go | 2 +- vault/logical_system.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/capabilities.go b/vault/capabilities.go index d9990d79e77a..a2b9e3f26eb3 100644 --- a/vault/capabilities.go +++ b/vault/capabilities.go @@ -44,7 +44,7 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string, } if entity != nil && entity.Disabled { - return nil, logical.ErrEntityDisabled + return nil, logical.ErrPermissionDenied } for _, item := range derivedPolicies { diff --git a/vault/logical_system.go b/vault/logical_system.go index 495f8e769cc5..9287933c6a8c 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -3490,7 +3490,7 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic return nil, err } if entity != nil && entity.Disabled { - return nil, logical.ErrEntityDisabled + return nil, logical.ErrPermissionDenied } }