vault: First pass at mount/unmount

This commit is contained in:
Armon Dadgar 2015-03-11 18:19:40 -07:00
parent 59052069bc
commit 0ca093fb2d
3 changed files with 202 additions and 5 deletions

View File

@ -14,6 +14,9 @@ var (
// ErrUnsupportedPath is returned if the path is not supported
// by the logical backend.
ErrUnsupportedPath = errors.New("unsupported path")
// ErrInvalidRequest is returned if the request is invalid
ErrInvalidRequest = errors.New("invalid request")
)
// LogicalBackend interface must be implemented to be "mountable" at
@ -70,6 +73,21 @@ type Request struct {
View *BarrierView
}
// Get returns a data field and guards for nil Data
func (r *Request) Get(key string) interface{} {
if r.Data == nil {
return nil
}
return r.Data[key]
}
// GetString returns a data field as a string
func (r *Request) GetString(key string) string {
raw := r.Get(key)
s, _ := raw.(string)
return s
}
// Response is a struct that stores the response of a request.
// It is used to abstract the details of the higher level request protocol.
type Response struct {
@ -122,3 +140,13 @@ func HelpResponse(text string, seeAlso []string) *Response {
},
}
}
// ErrorResponse is used to format an error response
func ErrorResponse(text string) *Response {
return &Response{
IsSecret: false,
Data: map[string]interface{}{
"error": text,
},
}
}

View File

@ -3,6 +3,8 @@ package vault
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
const (
@ -26,6 +28,17 @@ type MountTable struct {
Entries []*MountEntry `json:"entries"`
}
// Returns a deep copy of the mount table
func (t *MountTable) Clone() *MountTable {
mt := &MountTable{
Entries: make([]*MountEntry, len(t.Entries)),
}
for i, e := range t.Entries {
mt.Entries[i] = e.Clone()
}
return mt
}
// MountEntry is used to represent a mount table entry
type MountEntry struct {
Path string `json:"path"` // Mount Path
@ -34,6 +47,16 @@ type MountEntry struct {
UUID string `json:"uuid"` // Barrier view UUID
}
// Returns a deep copy of the mount entry
func (e *MountEntry) Clone() *MountEntry {
return &MountEntry{
Path: e.Path,
Type: e.Type,
Description: e.Description,
UUID: e.UUID,
}
}
// loadMounts is invoked as part of postUnseal to load the mount table
func (c *Core) loadMounts() error {
// Load the existing mount table
@ -57,16 +80,16 @@ func (c *Core) loadMounts() error {
// Create and persist the default mount table
c.mounts = defaultMountTable()
if err := c.persistMounts(); err != nil {
if err := c.persistMounts(c.mounts); err != nil {
return loadMountsFailed
}
return nil
}
// persistMounts is used to persist the mount table after modification
func (c *Core) persistMounts() error {
func (c *Core) persistMounts(table *MountTable) error {
// Marshal the table
raw, err := json.Marshal(c.mounts)
raw, err := json.Marshal(table)
if err != nil {
c.logger.Printf("[ERR] core: failed to encode mount table: %v", err)
return err
@ -115,6 +138,90 @@ func (c *Core) setupMounts() error {
return nil
}
// mountEntry is used to create a new mount entry
func (c *Core) mountEntry(me *MountEntry) error {
c.mountsLock.Lock()
defer c.mountsLock.Unlock()
// Ensure we end the path in a slash
if !strings.HasSuffix(me.Path, "/") {
me.Path += "/"
}
// Verify there is no conflicting mount
if match := c.router.MatchingMount(me.Path); match != "" {
return fmt.Errorf("existing mount at '%s'", match)
}
// Lookup the new backend
backend, err := NewBackend(me.Type, nil)
if err != nil {
return err
}
// Generate a new UUID and view
me.UUID = generateUUID()
view := NewBarrierView(c.barrier, backendBarrierPrefix+me.UUID+"/")
// Update the mount table
newTable := c.mounts.Clone()
newTable.Entries = append(newTable.Entries, me)
if err := c.persistMounts(newTable); err != nil {
return errors.New("failed to update mount table")
}
c.mounts = newTable
// Mount the backend
if err := c.router.Mount(backend, me.Type, me.Path, view); err != nil {
return err
}
c.logger.Printf("[INFO] core: mounted '%s' type: %s", me.Path, me.Type)
return nil
}
// unmountPath is used to unmount a path
func (c *Core) unmountPath(path string) error {
c.mountsLock.Lock()
defer c.mountsLock.Unlock()
// Ensure we end the path in a slash
if !strings.HasSuffix(path, "/") {
path += "/"
}
// Verify exact match of the route
match := c.router.MatchingMount(path)
if match == "" || path != match {
return fmt.Errorf("no matching mount")
}
// Remove the entry from the mount table
newTable := c.mounts.Clone()
n := len(newTable.Entries)
for i := 0; i < n; i++ {
if newTable.Entries[i].Path == path {
newTable.Entries[i], newTable.Entries[n-1] = newTable.Entries[n-1], nil
newTable.Entries = newTable.Entries[:n-1]
}
}
// Update the mount table
if err := c.persistMounts(newTable); err != nil {
return errors.New("failed to update mount table")
}
c.mounts = newTable
// Unmount the backend
if err := c.router.Unmount(path); err != nil {
return err
}
// TODO: Delete data in view?
// TODO: Handle revocation?
c.logger.Printf("[INFO] core: unmounted '%s'", path)
return nil
}
// defaultMountTable creates a default mount table
func defaultMountTable() *MountTable {
table := &MountTable{}

View File

@ -1,5 +1,7 @@
package vault
import "strings"
// SystemBackend implements the LogicalBackend interface but is used
// to interact with the core of the system. It acts like a "procfs"
// to provide a uniform interface to vault.
@ -12,13 +14,17 @@ func (s *SystemBackend) HandleRequest(req *Request) (*Response, error) {
switch {
case req.Path == "mounts":
return s.handleMountTable(req)
case strings.HasPrefix(req.Path, "mount/"):
return s.handleMountOperation(req)
default:
return nil, ErrUnsupportedPath
}
}
func (s *SystemBackend) RootPaths() []string {
return []string{}
return []string{
"mount/*",
}
}
// handleMountTable handles the "mounts" endpoint to provide the mount table
@ -26,7 +32,7 @@ func (s *SystemBackend) handleMountTable(req *Request) (*Response, error) {
switch req.Operation {
case ReadOperation:
case HelpOperation:
return HelpResponse("logical backend mount table", nil), nil
return HelpResponse("logical backend mount table", []string{"sys/mount/"}), nil
default:
return nil, ErrUnsupportedOperation
}
@ -47,3 +53,59 @@ func (s *SystemBackend) handleMountTable(req *Request) (*Response, error) {
}
return resp, nil
}
// handleMountOperation is used to mount or unmount a path
func (s *SystemBackend) handleMountOperation(req *Request) (*Response, error) {
switch req.Operation {
case WriteOperation:
return s.handleMount(req)
case DeleteOperation:
return s.handleUnmount(req)
case HelpOperation:
return HelpResponse("used to mount or unmount a path", []string{"sys/mounts"}), nil
default:
return nil, ErrUnsupportedOperation
}
}
// handleMount is used to mount a new path
func (s *SystemBackend) handleMount(req *Request) (*Response, error) {
suffix := strings.TrimPrefix(req.Path, "mount/")
if len(suffix) == 0 {
return ErrorResponse("path cannot be blank"), ErrInvalidRequest
}
// Get the type and description (optionally)
logicalType := req.GetString("type")
if logicalType == "" {
return ErrorResponse("backend type must be specified as a string"), ErrInvalidRequest
}
description := req.GetString("description")
// Create the mount entry
me := &MountEntry{
Path: suffix,
Type: logicalType,
Description: description,
}
// Attempt mount
if err := s.core.mountEntry(me); err != nil {
return ErrorResponse(err.Error()), ErrInvalidRequest
}
return nil, nil
}
// handleUnmount is used to unmount a path
func (s *SystemBackend) handleUnmount(req *Request) (*Response, error) {
suffix := strings.TrimPrefix(req.Path, "mount/")
if len(suffix) == 0 {
return ErrorResponse("path cannot be blank"), ErrInvalidRequest
}
// Attempt unmount
if err := s.core.unmountPath(suffix); err != nil {
return ErrorResponse(err.Error()), ErrInvalidRequest
}
return nil, nil
}