From dffcf0548ee7b30b011fd82d3e5973bb0e58a8c0 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 31 Aug 2015 14:27:49 -0400 Subject: [PATCH] Plumb per-mount config options through API --- api/sys_mounts.go | 22 +++++++++------ command/mount.go | 40 ++++++++++++++++++++++++-- command/remount.go | 34 +++++++++++++++++++++- http/handler_test.go | 10 ++++++- http/sys_mount_test.go | 40 ++++++++++++++++++++++++++ logical/testing/testing.go | 6 +++- vault/logical_system.go | 55 ++++++++++++++++++++++++++++++++++-- vault/logical_system_test.go | 15 ++++++++-- vault/mount.go | 9 ++++-- vault/mount_test.go | 6 ++-- 10 files changed, 212 insertions(+), 25 deletions(-) diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 422a90dccb..f22afd1920 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -2,6 +2,9 @@ package api import ( "fmt" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/vault" ) func (c *Sys) ListMounts() (map[string]*Mount, error) { @@ -17,15 +20,12 @@ func (c *Sys) ListMounts() (map[string]*Mount, error) { return result, err } -func (c *Sys) Mount(path, mountType, description string) error { +func (c *Sys) Mount(path string, mountInfo *Mount) error { if err := c.checkMountPath(path); err != nil { return err } - body := map[string]string{ - "type": mountType, - "description": description, - } + body := structs.Map(mountInfo) r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s", path)) if err := r.SetJSONBody(body); err != nil { @@ -54,7 +54,7 @@ func (c *Sys) Unmount(path string) error { return err } -func (c *Sys) Remount(from, to string) error { +func (c *Sys) Remount(from, to string, config *vault.MountConfig) error { if err := c.checkMountPath(from); err != nil { return err } @@ -62,10 +62,13 @@ func (c *Sys) Remount(from, to string) error { return err } - body := map[string]string{ + body := map[string]interface{}{ "from": from, "to": to, } + if config != nil { + body["config"] = *config + } r := c.c.NewRequest("POST", "/v1/sys/remount") if err := r.SetJSONBody(body); err != nil { @@ -88,6 +91,7 @@ func (c *Sys) checkMountPath(path string) error { } type Mount struct { - Type string - Description string + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Config *vault.MountConfig `json:"config" structs:"config"` } diff --git a/command/mount.go b/command/mount.go index 226e4d7c6a..dbe06f6698 100644 --- a/command/mount.go +++ b/command/mount.go @@ -3,6 +3,10 @@ package command import ( "fmt" "strings" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/vault" ) // MountCommand is a Command that mounts a new mount. @@ -11,10 +15,12 @@ type MountCommand struct { } func (c *MountCommand) Run(args []string) int { - var description, path string + var description, path, defaultLeaseTTL, maxLeaseTTL string flags := c.Meta.FlagSet("mount", FlagSetDefault) flags.StringVar(&description, "description", "", "") flags.StringVar(&path, "path", "", "") + flags.StringVar(&defaultLeaseTTL, "default_lease_ttl", "", "") + flags.StringVar(&maxLeaseTTL, "max_lease_ttl", "", "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { return 1 @@ -42,7 +48,37 @@ func (c *MountCommand) Run(args []string) int { return 2 } - if err := client.Sys().Mount(path, mountType, description); err != nil { + mountInfo := &api.Mount{ + Type: mountType, + Description: description, + Config: &vault.MountConfig{}, + } + + var passConfig bool + if defaultLeaseTTL != "" { + mountInfo.Config.DefaultLeaseTTL, err = time.ParseDuration(defaultLeaseTTL) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error parsing default lease TTL duration: %s", err)) + return 2 + } + passConfig = true + } + if maxLeaseTTL != "" { + mountInfo.Config.MaxLeaseTTL, err = time.ParseDuration(maxLeaseTTL) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error parsing max lease TTL duration: %s", err)) + return 2 + } + passConfig = true + } + + if !passConfig { + mountInfo.Config = nil + } + + if err := client.Sys().Mount(path, mountInfo); err != nil { c.Ui.Error(fmt.Sprintf( "Mount error: %s", err)) return 2 diff --git a/command/remount.go b/command/remount.go index 8cf529723f..c1480ff207 100644 --- a/command/remount.go +++ b/command/remount.go @@ -3,6 +3,9 @@ package command import ( "fmt" "strings" + "time" + + "github.com/hashicorp/vault/vault" ) // RemountCommand is a Command that remounts a mounted secret backend @@ -12,7 +15,10 @@ type RemountCommand struct { } func (c *RemountCommand) Run(args []string) int { + var defaultLeaseTTL, maxLeaseTTL string flags := c.Meta.FlagSet("remount", FlagSetDefault) + flags.StringVar(&defaultLeaseTTL, "default_lease_ttl", "", "") + flags.StringVar(&maxLeaseTTL, "max_lease_ttl", "", "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { return 1 @@ -29,6 +35,32 @@ func (c *RemountCommand) Run(args []string) int { from := args[0] to := args[1] + mountConfig := &vault.MountConfig{} + var err error + var passConfig bool + if defaultLeaseTTL != "" { + mountConfig.DefaultLeaseTTL, err = time.ParseDuration(defaultLeaseTTL) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error parsing default lease TTL duration: %s", err)) + return 1 + } + passConfig = true + } + if maxLeaseTTL != "" { + mountConfig.MaxLeaseTTL, err = time.ParseDuration(maxLeaseTTL) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error parsing max lease TTL duration: %s", err)) + return 1 + } + passConfig = true + } + + if !passConfig { + mountConfig = nil + } + client, err := c.Client() if err != nil { c.Ui.Error(fmt.Sprintf( @@ -36,7 +68,7 @@ func (c *RemountCommand) Run(args []string) int { return 2 } - if err := client.Sys().Remount(from, to); err != nil { + if err := client.Sys().Remount(from, to, mountConfig); err != nil { c.Ui.Error(fmt.Sprintf( "Unmount error: %s", err)) return 2 diff --git a/http/handler_test.go b/http/handler_test.go index 266c09a0af..9353199584 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -34,16 +34,24 @@ func TestSysMounts_headerAuth(t *testing.T) { "secret/": map[string]interface{}{ "description": "generic secret storage", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", "type": "system", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) + t.Fatalf("bad:\nExpected: %#v\nActual: %#v\n", expected, actual) } } diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 1f41837524..1ea3a21c26 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -20,10 +20,18 @@ func TestSysMounts(t *testing.T) { "secret/": map[string]interface{}{ "description": "generic secret storage", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", "type": "system", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, } testResponseStatus(t, resp, 200) @@ -52,14 +60,26 @@ func TestSysMount(t *testing.T) { "foo/": map[string]interface{}{ "description": "foo", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "secret/": map[string]interface{}{ "description": "generic secret storage", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", "type": "system", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, } testResponseStatus(t, resp, 200) @@ -110,14 +130,26 @@ func TestSysRemount(t *testing.T) { "bar/": map[string]interface{}{ "description": "foo", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "secret/": map[string]interface{}{ "description": "generic secret storage", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", "type": "system", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, } testResponseStatus(t, resp, 200) @@ -149,10 +181,18 @@ func TestSysUnmount(t *testing.T) { "secret/": map[string]interface{}{ "description": "generic secret storage", "type": "generic", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", "type": "system", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, }, } testResponseStatus(t, resp, 200) diff --git a/logical/testing/testing.go b/logical/testing/testing.go index b306a8e55a..e829f003a3 100644 --- a/logical/testing/testing.go +++ b/logical/testing/testing.go @@ -167,7 +167,11 @@ func Test(t TestT, c TestCase) { // Mount the backend prefix := "mnt" - if err := client.Sys().Mount(prefix, "test", "acceptance test"); err != nil { + mountInfo := &api.Mount{ + Type: "test", + Description: "acceptance test", + } + if err := client.Sys().Mount(prefix, mountInfo); err != nil { t.Fatal("error mounting backend: ", err) return } diff --git a/vault/logical_system.go b/vault/logical_system.go index f0978854fe..625ebae109 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -5,8 +5,10 @@ import ( "strings" "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + "github.com/mitchellh/mapstructure" ) var ( @@ -70,6 +72,10 @@ func NewSystemBackend(core *Core) logical.Backend { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["mount_desc"][0]), }, + "config": &framework.FieldSchema{ + Type: framework.TypeMap, + Description: strings.TrimSpace(sysHelp["mount_config"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -339,9 +345,10 @@ func (b *SystemBackend) handleMountTable( Data: make(map[string]interface{}), } for _, entry := range b.Core.mounts.Entries { - info := map[string]string{ + info := map[string]interface{}{ "type": entry.Type, "description": entry.Description, + "config": structs.Map(entry.Config), } resp.Data[entry.Path] = info } @@ -356,6 +363,24 @@ func (b *SystemBackend) handleMount( path := data.Get("path").(string) logicalType := data.Get("type").(string) description := data.Get("description").(string) + var config *MountConfig + configInt, ok := data.GetOk("config") + if ok { + configMap, ok := configInt.(map[string]interface{}) + if !ok { + return logical.ErrorResponse( + "cannot convert mount config information into proper values"), + logical.ErrInvalidRequest + } + if configMap != nil && len(configMap) != 0 { + err := mapstructure.Decode(configMap, config) + if err != nil { + return logical.ErrorResponse( + "unable to convert given mount config information"), + logical.ErrInvalidRequest + } + } + } if logicalType == "" { return logical.ErrorResponse( @@ -369,6 +394,9 @@ func (b *SystemBackend) handleMount( Type: logicalType, Description: description, } + if config != nil { + me.Config = *config + } // Attempt mount if err := b.Core.mount(me); err != nil { @@ -418,9 +446,27 @@ func (b *SystemBackend) handleRemount( "both 'from' and 'to' path must be specified as a string"), logical.ErrInvalidRequest } + var config *MountConfig + configInt, ok := data.GetOk("config") + if ok { + configMap, ok := configInt.(map[string]interface{}) + if !ok { + return logical.ErrorResponse( + "cannot convert mount config information into proper values"), + logical.ErrInvalidRequest + } + if configMap != nil && len(configMap) != 0 { + err := mapstructure.Decode(configMap, config) + if err != nil { + return logical.ErrorResponse( + "unable to convert given mount config information"), + logical.ErrInvalidRequest + } + } + } // Attempt remount - if err := b.Core.remount(fromPath, toPath); err != nil { + if err := b.Core.remount(fromPath, toPath, config); err != nil { b.Backend.Logger().Printf("[ERR] sys: remount '%s' to '%s' failed: %v", fromPath, toPath, err) return handleError(err) } @@ -830,6 +876,11 @@ west coast. "", }, + "mount_config": { + `Configuration for this mount, such as default_lease_ttl +and max_lease_ttl.`, + }, + "remount": { "Move the mount point of an already-mounted backend.", ` diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index df2c02037f..1190d72001 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -3,6 +3,7 @@ package vault import ( "reflect" "testing" + "time" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/logical" @@ -39,17 +40,25 @@ func TestSystemBackend_mounts(t *testing.T) { } exp := map[string]interface{}{ - "secret/": map[string]string{ + "secret/": map[string]interface{}{ "type": "generic", "description": "generic secret storage", + "config": map[string]interface{}{ + "default_lease_ttl": time.Duration(0), + "max_lease_ttl": time.Duration(0), + }, }, - "sys/": map[string]string{ + "sys/": map[string]interface{}{ "type": "system", "description": "system endpoints used for control, policy and debugging", + "config": map[string]interface{}{ + "default_lease_ttl": time.Duration(0), + "max_lease_ttl": time.Duration(0), + }, }, } if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("got: %#v expect: %#v", resp.Data, exp) + t.Fatalf("Got:\n%#v\nExpected:\n%#v", resp.Data, exp) } } diff --git a/vault/mount.go b/vault/mount.go index d8886defb4..9c00b7542d 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -118,8 +118,8 @@ type MountEntry struct { // MountConfig is used to hold settable options type MountConfig struct { - DefaultLeaseTTL time.Duration `json:"default_lease_ttl"` // Override for global default - MaxLeaseTTL time.Duration `json:"max_lease_ttl"` // Override for global default + DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl"` // Override for global default + MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl"` // Override for global default } // Returns a deep copy of the mount entry @@ -283,7 +283,7 @@ func (c *Core) taintMountEntry(path string) error { } // Remount is used to remount a path at a new mount point. -func (c *Core) remount(src, dst string) error { +func (c *Core) remount(src, dst string, config *MountConfig) error { c.mounts.Lock() defer c.mounts.Unlock() @@ -339,6 +339,9 @@ func (c *Core) remount(src, dst string) error { if ent.Path == src { ent.Path = dst ent.Tainted = false + if config != nil { + ent.Config = *config + } break } } diff --git a/vault/mount_test.go b/vault/mount_test.go index 683282b42c..dcded5d09a 100644 --- a/vault/mount_test.go +++ b/vault/mount_test.go @@ -192,7 +192,7 @@ func TestCore_Unmount_Cleanup(t *testing.T) { func TestCore_Remount(t *testing.T) { c, key, _ := TestCoreUnsealed(t) - err := c.remount("secret", "foo") + err := c.remount("secret", "foo", nil) if err != nil { t.Fatalf("err: %v", err) } @@ -280,7 +280,7 @@ func TestCore_Remount_Cleanup(t *testing.T) { } // Remount, this should cleanup - if err := c.remount("test/", "new/"); err != nil { + if err := c.remount("test/", "new/", nil); err != nil { t.Fatalf("err: %v", err) } @@ -309,7 +309,7 @@ func TestCore_Remount_Cleanup(t *testing.T) { func TestCore_Remount_Protected(t *testing.T) { c, _, _ := TestCoreUnsealed(t) - err := c.remount("sys", "foo") + err := c.remount("sys", "foo", nil) if err.Error() != "cannot remount 'sys/'" { t.Fatalf("err: %v", err) }