vault: convert to logical.Request and friends

This commit is contained in:
Mitchell Hashimoto 2015-03-15 14:53:41 -07:00
parent 5ffcd02b7a
commit d1d1929192
17 changed files with 104 additions and 1001 deletions

View File

@ -1,5 +1,9 @@
package logical
import (
"errors"
)
// Request is a struct that stores the parameters and context
// of a request being made to Vault. It is used to abstract
// the details of the higher level request protocol from the handlers.
@ -47,3 +51,16 @@ const (
RevokeOperation = "revoke"
HelpOperation = "help"
)
var (
// ErrUnsupportedOperation is returned if the operation is not supported
// by the logical backend.
ErrUnsupportedOperation = errors.New("unsupported operation")
// 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")
)

View File

@ -9,6 +9,7 @@ import (
"os"
"sync"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/physical"
"github.com/hashicorp/vault/shamir"
)
@ -149,7 +150,7 @@ func NewCore(conf *CoreConfig) (*Core, error) {
}
// HandleRequest is used to handle a new incoming request
func (c *Core) HandleRequest(req *Request) (*Response, error) {
func (c *Core) HandleRequest(req *logical.Request) (*logical.Response, error) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if c.sealed {

View File

@ -4,6 +4,7 @@ import (
"reflect"
"testing"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/physical"
)
@ -272,8 +273,8 @@ func TestCore_Route_Sealed(t *testing.T) {
}
// Should not route anything
req := &Request{
Operation: ReadOperation,
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/mounts",
}
_, err := c.HandleRequest(req)

View File

@ -136,14 +136,14 @@ func (m *ExpirationManager) RevokePrefix(prefix string) error {
// Renew is used to renew a secret using the given vaultID
// and a renew interval. The increment may be ignored.
func (m *ExpirationManager) Renew(vaultID string, increment time.Duration) (*Lease, error) {
func (m *ExpirationManager) Renew(vaultID string, increment time.Duration) (*logical.Lease, error) {
return nil, nil
}
// Register is used to take a request and response with an associated
// lease. The secret gets assigned a vaultId and the management of
// of lease is assumed by the expiration manager.
func (m *ExpirationManager) Register(req *Request, resp *Response) (string, error) {
func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Response) (string, error) {
// Ignore if there is no lease
if resp == nil || resp.Lease == nil {
return "", nil
@ -195,7 +195,7 @@ type leaseEntry struct {
VaultID string
Path string
Data map[string]interface{}
Lease *Lease
Lease *logical.Lease
IssueTime time.Time
RenewTime time.Time
}

View File

@ -5,12 +5,28 @@ import (
"strings"
"testing"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/physical"
)
// mockExpiration returns a mock expiration manager
func mockExpiration(t *testing.T) *ExpirationManager {
inm := physical.NewInmem()
b, err := NewAESGCMBarrier(inm)
if err != nil {
t.Fatalf("err: %v", err)
}
// Initialize and unseal
key, _ := b.GenerateKey()
b.Initialize(key)
b.Unseal(key)
// Create the barrier view
view := NewBarrierView(b, "expire/")
router := NewRouter()
view := mockView(t, "expire/")
return NewExpirationManager(router, view)
}
@ -34,13 +50,13 @@ func TestExpiration_StartStop(t *testing.T) {
func TestExpiration_Register(t *testing.T) {
exp := mockExpiration(t)
req := &Request{
Operation: ReadOperation,
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "prod/aws/foo",
}
resp := &Response{
resp := &logical.Response{
IsSecret: true,
Lease: &Lease{
Lease: &logical.Lease{
Duration: time.Hour,
MaxDuration: time.Hour,
},
@ -71,7 +87,7 @@ func TestLeaseEntry(t *testing.T) {
Data: map[string]interface{}{
"testing": true,
},
Lease: &Lease{
Lease: &logical.Lease{
Renewable: true,
Duration: time.Minute,
MaxDuration: time.Hour,

View File

@ -1,152 +0,0 @@
package vault
import (
"encoding/json"
"fmt"
"time"
"github.com/hashicorp/vault/logical"
)
// GenericBackend is used for the storing generic secrets. These are not
// materialized in any way. The value that is written to this backend
// is the same value that is always returned. Leasing can be configured on
// a per-key basis.
type GenericBackend struct{}
// newGenericBackend is a factory constructor for the generic backend
func newGenericBackend(map[string]string) (LogicalBackend, error) {
b := &GenericBackend{}
return b, nil
}
// HandleRequest is used to handle a request and generate a response.
// The backends must check the operation type and handle appropriately.
func (g *GenericBackend) HandleRequest(req *Request) (*Response, error) {
switch req.Operation {
case ReadOperation:
return g.handleRead(req)
case WriteOperation:
return g.handleWrite(req)
case ListOperation:
return g.handleList(req)
case DeleteOperation:
return g.handleDelete(req)
case HelpOperation:
return g.handleHelp(req)
default:
return nil, ErrUnsupportedOperation
}
}
// RootPaths is a list of paths that require root level privileges,
// which do not exist for the geneirc backend.
func (g *GenericBackend) RootPaths() []string {
return nil
}
func (g *GenericBackend) handleRead(req *Request) (*Response, error) {
// Read the path
out, err := req.Storage.Get(req.Path)
if err != nil {
return nil, fmt.Errorf("read failed: %v", err)
}
// Fast-path the no data case
if out == nil {
return nil, nil
}
// Decode the data
var raw map[string]interface{}
if err := json.Unmarshal(out.Value, &raw); err != nil {
return nil, fmt.Errorf("json decoding failed: %v", err)
}
// Check if there is a lease key
leaseVal, ok := raw["lease"].(string)
var lease *Lease
if ok {
leaseDuration, err := time.ParseDuration(leaseVal)
if err == nil {
lease = &Lease{
Renewable: false,
Revokable: false,
Duration: leaseDuration,
MaxDuration: leaseDuration,
MaxIncrement: 0,
}
}
}
// Generate the response
resp := &Response{
IsSecret: true,
Lease: lease,
Data: raw,
}
return resp, nil
}
func (g *GenericBackend) handleWrite(req *Request) (*Response, error) {
// Check that some fields are given
if len(req.Data) == 0 {
return nil, fmt.Errorf("missing data fields")
}
// JSON encode the data
buf, err := json.Marshal(req.Data)
if err != nil {
return nil, fmt.Errorf("json encoding failed: %v", err)
}
// Write out a new key
entry := &logical.StorageEntry{
Key: req.Path,
Value: buf,
}
if err := req.Storage.Put(entry); err != nil {
return nil, fmt.Errorf("failed to write: %v", err)
}
return nil, nil
}
func (g *GenericBackend) handleDelete(req *Request) (*Response, error) {
// Delete the key at the request path
if err := req.Storage.Delete(req.Path); err != nil {
return nil, err
}
return nil, nil
}
func (g *GenericBackend) handleList(req *Request) (*Response, error) {
// List the keys at the prefix given by the request
keys, err := req.Storage.List(req.Path)
if err != nil {
return nil, err
}
// Generate the response
resp := &Response{
IsSecret: false,
Lease: nil,
Data: map[string]interface{}{
"keys": keys,
},
}
return resp, nil
}
func (g *GenericBackend) handleHelp(req *Request) (*Response, error) {
resp := &Response{
IsSecret: false,
Lease: nil,
Data: map[string]interface{}{
"help": genericHelpText,
},
}
return resp, nil
}
// genericHelpText is the help information we return
const genericHelpText = "Generic backend for storing and retreiving raw keys with user-defined fields"

View File

@ -1,228 +0,0 @@
package vault
import (
"testing"
"time"
"github.com/hashicorp/vault/physical"
)
// mockView returns a view attached to a barrier / backend
func mockView(t *testing.T, prefix string) *BarrierView {
inm := physical.NewInmem()
b, err := NewAESGCMBarrier(inm)
if err != nil {
t.Fatalf("err: %v", err)
}
// Initialize and unseal
key, _ := b.GenerateKey()
b.Initialize(key)
b.Unseal(key)
// Create the barrier view
view := NewBarrierView(b, prefix)
return view
}
// mockRequest returns a request with a real view attached
func mockRequest(t *testing.T, op Operation, path string) *Request {
view := mockView(t, "logical/")
// Create the request
req := &Request{
Operation: op,
Path: path,
Data: make(map[string]interface{}),
Storage: view,
}
return req
}
func TestGenericBackend_RootPaths(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
root := b.RootPaths()
if len(root) != 0 {
t.Fatalf("unexpected: %v", root)
}
}
func TestGenericBackend_Write(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, WriteOperation, "foo")
req.Data["raw"] = "test"
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
out, err := req.Storage.Get("foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if out == nil {
t.Fatalf("failed to write to view")
}
}
func TestGenericBackend_Read(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, WriteOperation, "foo")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, ReadOperation, "foo")
req2.Storage = req.Storage
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if !resp.IsSecret {
t.Fatalf("should be secret: %#v", resp)
}
if resp.Lease == nil {
t.Fatalf("should have lease: %#v", resp)
}
if resp.Lease.Renewable {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.Revokable {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.Duration != time.Hour {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.MaxDuration != time.Hour {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.MaxIncrement != 0 {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Data["raw"] != "test" {
t.Fatalf("bad data: %#v", resp.Data)
}
if resp.Data["lease"] != "1h" {
t.Fatalf("bad data: %#v", resp.Data)
}
}
func TestGenericBackend_Delete(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, WriteOperation, "foo")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, DeleteOperation, "foo")
req2.Storage = req.Storage
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
req3 := mockRequest(t, ReadOperation, "foo")
req3.Storage = req.Storage
resp, err = b.HandleRequest(req3)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestGenericBackend_List(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, WriteOperation, "foo/bar")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, ListOperation, "")
req2.Storage = req.Storage
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.IsSecret {
t.Fatalf("bad: %v", resp)
}
if resp.Lease != nil {
t.Fatalf("bad: %v", resp)
}
if resp.Data["keys"] == nil {
t.Fatalf("bad: %v", resp)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 1 || keys[0] != "foo/" {
t.Fatalf("keys: %v", keys)
}
}
func TestGenericBackend_Help(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, HelpOperation, "foo")
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.IsSecret {
t.Fatalf("bad: %v", resp)
}
if resp.Lease != nil {
t.Fatalf("bad: %v", resp)
}
if resp.Data["help"] != genericHelpText {
t.Fatalf("bad: %v", resp)
}
}

View File

@ -1,171 +0,0 @@
package vault
import (
"errors"
"fmt"
"time"
"github.com/hashicorp/vault/logical"
)
var (
// ErrUnsupportedOperation is returned if the operation is not supported
// by the logical backend.
ErrUnsupportedOperation = errors.New("unsupported operation")
// 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
// a given path. Requests flow through a router which has various mount
// points that flow to a logical backend. The logic of each backend is flexible,
// and this is what allows materialized keys to function. There can be specialized
// logical backends for various upstreams (Consul, PostgreSQL, MySQL, etc) that can
// interact with remote APIs to generate keys dynamically. This interface also
// allows for a "procfs" like interaction, as internal state can be exposed by
// acting like a logical backend and being mounted.
type LogicalBackend interface {
// HandleRequest is used to handle a request and generate a response.
// The backends must check the operation type and handle appropriately.
HandleRequest(*Request) (*Response, error)
// RootPaths is a list of paths that require root level privileges.
// These paths will be enforced by the router so that backends do
// not need to handle the authorization. Paths are enforced exactly
// or using a prefix match if they end in '*'
RootPaths() []string
}
// Operation is an enum that is used to specify the type
// of request being made
type Operation string
const (
ReadOperation Operation = "read"
WriteOperation = "write"
DeleteOperation = "delete"
ListOperation = "list"
RevokeOperation = "revoke"
HelpOperation = "help"
)
// Request is a struct that stores the parameters and context
// of a request being made to Vault. It is used to abstract
// the details of the higher level request protocol from the handlers.
type Request struct {
// Operation is the requested operation type
Operation Operation
// Path is the part of the request path not consumed by the
// routing. As an example, if the original request path is "prod/aws/foo"
// and the AWS logical backend is mounted at "prod/aws/", then the
// final path is "foo" since the mount prefix is trimmed.
Path string
// Request data is an opaque map that must have string keys.
Data map[string]interface{}
// View is the storage view of this logical backend. It can be used
// to durably store and retrieve state from the backend.
Storage logical.Storage
}
// 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 {
// IsSecret is used to indicate this is secret material instead of policy or configuration.
// Non-secrets never have a VaultID or renewable properties.
IsSecret bool
// The lease settings if applicable.
Lease *Lease
// Response data is an opaque map that must have string keys.
Data map[string]interface{}
}
// Lease is used to provide more information about the lease
type Lease struct {
VaultID string // VaultID is the unique identifier used for renewal and revocation
Renewable bool // Is the VaultID renewable
Revokable bool // Is the secret revokable. Must support 'Revoke' operation.
Duration time.Duration // Current lease duration
MaxDuration time.Duration // Maximum lease duration
MaxIncrement time.Duration // Maximum increment to lease duration
}
// Validate is used to sanity check a lease
func (l *Lease) Validate() error {
if l.Duration <= 0 {
return fmt.Errorf("lease duration must be greater than zero")
}
if l.MaxDuration <= 0 {
return fmt.Errorf("maximum lease duration must be greater than zero")
}
if l.Duration > l.MaxDuration {
return fmt.Errorf("lease duration cannot be greater than maximum lease duration")
}
if l.MaxIncrement < 0 {
return fmt.Errorf("maximum lease increment cannot be negative")
}
return nil
}
// Factory is the factory function to create a logical backend.
type Factory func(map[string]string) (LogicalBackend, error)
// BuiltinBackends contains all of the available backends
var BuiltinBackends = map[string]Factory{
"generic": newGenericBackend,
}
// NewBackend returns a new logical Backend with the given type and configuration.
// The backend is looked up in the BuiltinBackends variable.
func NewBackend(t string, conf map[string]string) (LogicalBackend, error) {
f, ok := BuiltinBackends[t]
if !ok {
return nil, fmt.Errorf("unknown logical backend type: %s", t)
}
return f(conf)
}
// HelpResponse is used to format a help response
func HelpResponse(text string, seeAlso []string) *Response {
return &Response{
IsSecret: false,
Data: map[string]interface{}{
"help": text,
"see_also": seeAlso,
},
}
}
// 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

@ -1,34 +0,0 @@
package vault
import (
"testing"
"time"
)
func TestLease_Validate(t *testing.T) {
l := &Lease{}
if err := l.Validate(); err.Error() != "lease duration must be greater than zero" {
t.Fatalf("err: %v", err)
}
l.Duration = time.Minute
if err := l.Validate(); err.Error() != "maximum lease duration must be greater than zero" {
t.Fatalf("err: %v", err)
}
l.MaxDuration = time.Second
if err := l.Validate(); err.Error() != "lease duration cannot be greater than maximum lease duration" {
t.Fatalf("err: %v", err)
}
l.MaxDuration = time.Minute
l.MaxIncrement = -1 * time.Second
if err := l.Validate(); err.Error() != "maximum lease increment cannot be negative" {
t.Fatalf("err: %v", err)
}
l.MaxIncrement = time.Second
if err := l.Validate(); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -8,6 +8,11 @@ import (
"github.com/hashicorp/vault/logical"
)
// logical.Factory
func PassthroughBackendFactory(map[string]string) (logical.Backend, error) {
return new(PassthroughBackend), nil
}
// PassthroughBackend is used storing secrets directly into the physical
// backend. The secrest are encrypted in the durable storage and custom lease
// information can be specified, but otherwise this backend doesn't do anything
@ -27,7 +32,7 @@ func (b *PassthroughBackend) HandleRequest(req *logical.Request) (*logical.Respo
case logical.ListOperation:
return b.handleList(req)
default:
return nil, ErrUnsupportedOperation
return nil, logical.ErrUnsupportedOperation
}
}

View File

@ -23,7 +23,7 @@ func (b *SystemBackend2) HandleRequest(req *logical.Request) (*logical.Response,
case req.Path == "remount":
return b.handleRemount(req)
default:
return nil, ErrUnsupportedPath
return nil, logical.ErrUnsupportedPath
}
}
@ -39,7 +39,7 @@ func (b *SystemBackend2) handleMountTable(req *logical.Request) (*logical.Respon
switch req.Operation {
case logical.ReadOperation:
default:
return nil, ErrUnsupportedOperation
return nil, logical.ErrUnsupportedOperation
}
b.Core.mountsLock.RLock()
@ -68,7 +68,7 @@ func (b *SystemBackend2) handleMountOperation(req *logical.Request) (*logical.Re
case logical.DeleteOperation:
return b.handleUnmount(req)
default:
return nil, ErrUnsupportedOperation
return nil, logical.ErrUnsupportedOperation
}
}
@ -76,13 +76,13 @@ func (b *SystemBackend2) handleMountOperation(req *logical.Request) (*logical.Re
func (b *SystemBackend2) handleMount(req *logical.Request) (*logical.Response, error) {
suffix := strings.TrimPrefix(req.Path, "mount/")
if len(suffix) == 0 {
return logical.ErrorResponse("path cannot be blank"), ErrInvalidRequest
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
}
// Get the type and description (optionally)
logicalType := req.GetString("type")
if logicalType == "" {
return logical.ErrorResponse("backend type must be specified as a string"), ErrInvalidRequest
return logical.ErrorResponse("backend type must be specified as a string"), logical.ErrInvalidRequest
}
description := req.GetString("description")
@ -95,7 +95,7 @@ func (b *SystemBackend2) handleMount(req *logical.Request) (*logical.Response, e
// Attempt mount
if err := b.Core.mount(me); err != nil {
return logical.ErrorResponse(err.Error()), ErrInvalidRequest
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
@ -104,12 +104,12 @@ func (b *SystemBackend2) handleMount(req *logical.Request) (*logical.Response, e
func (b *SystemBackend2) handleUnmount(req *logical.Request) (*logical.Response, error) {
suffix := strings.TrimPrefix(req.Path, "mount/")
if len(suffix) == 0 {
return logical.ErrorResponse("path cannot be blank"), ErrInvalidRequest
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
}
// Attempt unmount
if err := b.Core.unmount(suffix); err != nil {
return logical.ErrorResponse(err.Error()), ErrInvalidRequest
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
@ -119,9 +119,9 @@ func (b *SystemBackend2) handleUnmount(req *logical.Request) (*logical.Response,
func (b *SystemBackend2) handleRemount(req *logical.Request) (*logical.Response, error) {
// Only accept write operations
switch req.Operation {
case WriteOperation:
case logical.WriteOperation:
default:
return nil, ErrUnsupportedOperation
return nil, logical.ErrUnsupportedOperation
}
// Get the paths
@ -130,12 +130,12 @@ func (b *SystemBackend2) handleRemount(req *logical.Request) (*logical.Response,
if fromPath == "" || toPath == "" {
return logical.ErrorResponse(
"both 'from' and 'to' path must be specified as a string"),
ErrInvalidRequest
logical.ErrInvalidRequest
}
// Attempt remount
if err := b.Core.remount(fromPath, toPath); err != nil {
return logical.ErrorResponse(err.Error()), ErrInvalidRequest
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil

View File

@ -68,7 +68,7 @@ func TestSystemBackend_mount_invalid(t *testing.T) {
req := logical.TestRequest(t, logical.WriteOperation, "mount/prod/secret/")
req.Data["type"] = "nope"
resp, err := b.HandleRequest(req)
if err != ErrInvalidRequest {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "unknown logical backend type: nope" {
@ -94,7 +94,7 @@ func TestSystemBackend_unmount_invalid(t *testing.T) {
req := logical.TestRequest(t, logical.DeleteOperation, "mount/foo/")
resp, err := b.HandleRequest(req)
if err != ErrInvalidRequest {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "no matching mount" {
@ -124,7 +124,7 @@ func TestSystemBackend_remount_invalid(t *testing.T) {
req.Data["from"] = "unknown"
req.Data["to"] = "foo"
resp, err := b.HandleRequest(req)
if err != ErrInvalidRequest {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "no matching mount at 'unknown/'" {
@ -139,7 +139,7 @@ func TestSystemBackend_remount_system(t *testing.T) {
req.Data["from"] = "sys"
req.Data["to"] = "foo"
resp, err := b.HandleRequest(req)
if err != ErrInvalidRequest {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "cannot remount 'sys/'" {

View File

@ -5,8 +5,29 @@ import (
"errors"
"fmt"
"strings"
"github.com/hashicorp/vault/logical"
)
// TEMPORARY!
// BuiltinBackends contains all of the available backends
var BuiltinBackends = map[string]logical.Factory{
"generic": PassthroughBackendFactory,
}
// NewBackend returns a new logical Backend with the given type and configuration.
// The backend is looked up in the BuiltinBackends variable.
func NewBackend(t string, conf map[string]string) (logical.Backend, error) {
f, ok := BuiltinBackends[t]
if !ok {
return nil, fmt.Errorf("unknown logical backend type: %s", t)
}
return f(conf)
}
// TEMPORARY!
const (
// coreMountConfigPath is used to store the mount configuration.
// Mounts are protected within the Vault itself, which means they
@ -262,13 +283,13 @@ func (c *Core) persistMounts(table *MountTable) error {
// setupMounts is invoked after we've loaded the mount table to
// initialize the logical backends and setup the router
func (c *Core) setupMounts() error {
var backend LogicalBackend
var backend logical.Backend
var view *BarrierView
var err error
for _, entry := range c.mounts.Entries {
// Initialize the backend, special casing for system
if entry.Type == "system" {
backend = &SystemBackend{core: c}
backend = &SystemBackend2{Core: c}
view = NewBarrierView(c.barrier, systemBarrierPrefix+entry.UUID+"/")
c.systemView = view

View File

@ -6,6 +6,7 @@ import (
"sync"
"github.com/armon/go-radix"
"github.com/hashicorp/vault/logical"
)
// Router is used to do prefix based routing of a request to a logical backend
@ -25,13 +26,13 @@ func NewRouter() *Router {
// mountEntry is used to represent a mount point
type mountEntry struct {
mtype string
backend LogicalBackend
backend logical.Backend
view *BarrierView
rootPaths *radix.Tree
}
// Mount is used to expose a logical backend at a given prefix
func (r *Router) Mount(backend LogicalBackend, mtype, prefix string, view *BarrierView) error {
func (r *Router) Mount(backend logical.Backend, mtype, prefix string, view *BarrierView) error {
r.l.Lock()
defer r.l.Unlock()
@ -103,7 +104,7 @@ func (r *Router) MatchingMount(path string) string {
}
// Route is used to route a given request
func (r *Router) Route(req *Request) (*Response, error) {
func (r *Router) Route(req *logical.Request) (*logical.Response, error) {
// Find the mount point
r.l.RLock()
mount, raw, ok := r.root.LongestPrefix(req.Path)

View File

@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/vault/logical"
)
type NoopBackend struct {
@ -11,11 +13,12 @@ type NoopBackend struct {
Paths []string
}
func (n *NoopBackend) HandleRequest(req *Request) (*Response, error) {
func (n *NoopBackend) HandleRequest(req *logical.Request) (*logical.Response, error) {
n.Paths = append(n.Paths, req.Path)
if req.Storage == nil {
return nil, fmt.Errorf("missing view")
}
return nil, nil
}
@ -47,7 +50,7 @@ func TestRouter_Mount(t *testing.T) {
t.Fatalf("bad: %s", path)
}
req := &Request{
req := &logical.Request{
Path: "prod/aws/foo",
}
resp, err := r.Route(req)
@ -80,7 +83,7 @@ func TestRouter_Unmount(t *testing.T) {
t.Fatalf("err: %v", err)
}
req := &Request{
req := &logical.Request{
Path: "prod/aws/foo",
}
_, err = r.Route(req)
@ -110,7 +113,7 @@ func TestRouter_Remount(t *testing.T) {
t.Fatalf("err: %v", err)
}
req := &Request{
req := &logical.Request{
Path: "prod/aws/foo",
}
_, err = r.Route(req)
@ -118,7 +121,7 @@ func TestRouter_Remount(t *testing.T) {
t.Fatalf("err: %v", err)
}
req = &Request{
req = &logical.Request{
Path: "stage/aws/foo",
}
_, err = r.Route(req)

View File

@ -1,139 +0,0 @@
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.
type SystemBackend struct {
core *Core
}
func (s *SystemBackend) HandleRequest(req *Request) (*Response, error) {
// Switch on the path to route to the appropriate handler
switch {
case req.Path == "mounts":
return s.handleMountTable(req)
case strings.HasPrefix(req.Path, "mount/"):
return s.handleMountOperation(req)
case req.Path == "remount":
return s.handleRemount(req)
default:
return nil, ErrUnsupportedPath
}
}
func (s *SystemBackend) RootPaths() []string {
return []string{
"mount/*",
"remount",
}
}
// handleMountTable handles the "mounts" endpoint to provide the mount table
func (s *SystemBackend) handleMountTable(req *Request) (*Response, error) {
switch req.Operation {
case ReadOperation:
case HelpOperation:
return HelpResponse("logical backend mount table", []string{"sys/mount/"}), nil
default:
return nil, ErrUnsupportedOperation
}
s.core.mountsLock.RLock()
defer s.core.mountsLock.RUnlock()
resp := &Response{
IsSecret: false,
Data: make(map[string]interface{}),
}
for _, entry := range s.core.mounts.Entries {
info := map[string]string{
"type": entry.Type,
"description": entry.Description,
}
resp.Data[entry.Path] = info
}
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.mount(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.unmount(suffix); err != nil {
return ErrorResponse(err.Error()), ErrInvalidRequest
}
return nil, nil
}
// handleRemount is used to remount a path
func (s *SystemBackend) handleRemount(req *Request) (*Response, error) {
// Only accept write operations
switch req.Operation {
case WriteOperation:
case HelpOperation:
return HelpResponse("remount a backend path", []string{"sys/mount/", "sys/mounts"}), nil
default:
return nil, ErrUnsupportedOperation
}
// Get the paths
fromPath := req.GetString("from")
toPath := req.GetString("to")
if fromPath == "" || toPath == "" {
return ErrorResponse("both 'from' and 'to' path must be specified as a string"), ErrInvalidRequest
}
// Attempt remount
if err := s.core.remount(fromPath, toPath); err != nil {
return ErrorResponse(err.Error()), ErrInvalidRequest
}
return nil, nil
}

View File

@ -1,238 +0,0 @@
package vault
import (
"reflect"
"testing"
)
func testSystem(t *testing.T) *SystemBackend {
c, _ := TestCoreUnsealed(t)
return &SystemBackend{c}
}
func TestSystem_verifyRoot(t *testing.T) {
s := testSystem(t)
r := NewRouter()
r.Mount(s, "system", "sys/", nil)
root := []string{
"sys/mount/prod/",
"sys/remount",
}
nonRoot := []string{
"sys/mounts",
}
for _, key := range root {
if !r.RootPath(key) {
t.Fatalf("expected '%s' to be root path", key)
}
}
for _, key := range nonRoot {
if r.RootPath(key) {
t.Fatalf("expected '%s' to be non-root path", key)
}
}
}
func TestSystem_mounts(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: ReadOperation,
Path: "mounts",
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
exp := map[string]interface{}{
"secret/": map[string]string{
"type": "generic",
"description": "generic secret storage",
},
"sys/": map[string]string{
"type": "system",
"description": "system endpoints used for control, policy and debugging",
},
}
if !reflect.DeepEqual(resp.Data, exp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)
}
req = &Request{
Operation: HelpOperation,
Path: "mounts",
}
resp, err = s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.Data["help"] != "logical backend mount table" {
t.Fatalf("got: %#v", resp.Data)
}
}
func TestSystem_mount_help(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: HelpOperation,
Path: "mount/prod/secret/",
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.Data["help"] != "used to mount or unmount a path" {
t.Fatalf("got: %#v", resp.Data)
}
}
func TestSystem_mount(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: WriteOperation,
Path: "mount/prod/secret/",
Data: map[string]interface{}{
"type": "generic",
},
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_mount_invalid(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: WriteOperation,
Path: "mount/prod/secret/",
Data: map[string]interface{}{
"type": "what",
},
}
resp, err := s.HandleRequest(req)
if err != ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "unknown logical backend type: what" {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_unmount(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: DeleteOperation,
Path: "mount/secret/",
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_unmount_invalid(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: DeleteOperation,
Path: "mount/foo/",
}
resp, err := s.HandleRequest(req)
if err != ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "no matching mount" {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_remount(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: WriteOperation,
Path: "remount",
Data: map[string]interface{}{
"from": "secret",
"to": "foo",
},
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_remount_invalid(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: WriteOperation,
Path: "remount",
Data: map[string]interface{}{
"from": "unknown",
"to": "foo",
},
}
resp, err := s.HandleRequest(req)
if err != ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "no matching mount at 'unknown/'" {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_remount_system(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: WriteOperation,
Path: "remount",
Data: map[string]interface{}{
"from": "sys",
"to": "foo",
},
}
resp, err := s.HandleRequest(req)
if err != ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "cannot remount 'sys/'" {
t.Fatalf("bad: %v", resp)
}
}
func TestSystem_remount_help(t *testing.T) {
s := testSystem(t)
req := &Request{
Operation: HelpOperation,
Path: "remount",
}
resp, err := s.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.Data["help"] != "remount a backend path" {
t.Fatalf("got: %#v", resp.Data)
}
}