Plumb per-mount config options through API

This commit is contained in:
Jeff Mitchell 2015-08-31 14:27:49 -04:00
parent 893d2d9b00
commit 696d0c7b1d
10 changed files with 212 additions and 25 deletions

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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.",
`

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}