diff --git a/logical/logical.go b/logical/logical.go new file mode 100644 index 000000000..ae33c7219 --- /dev/null +++ b/logical/logical.go @@ -0,0 +1,24 @@ +package logical + +// Backend 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 Backend 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 +} + +// Factory is the factory function to create a logical backend. +type Factory func(map[string]string) (Backend, error) diff --git a/logical/request.go b/logical/request.go new file mode 100644 index 000000000..f3a7b518a --- /dev/null +++ b/logical/request.go @@ -0,0 +1,49 @@ +package logical + +// 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{} + + // Storage can be used to durably store and retrieve state. + Storage 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 +} + +// 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" +) diff --git a/logical/response.go b/logical/response.go new file mode 100644 index 000000000..2b491a431 --- /dev/null +++ b/logical/response.go @@ -0,0 +1,68 @@ +package logical + +import ( + "fmt" + "time" +) + +// 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 +} + +// 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, + }, + } +} diff --git a/logical/storage.go b/logical/storage.go new file mode 100644 index 000000000..2e7b7d9a2 --- /dev/null +++ b/logical/storage.go @@ -0,0 +1,15 @@ +package logical + +// Storage is the way that logical backends are able read/write data. +type Storage interface { + List(prefix string) ([]string, error) + Get(string) (*StorageEntry, error) + Put(*StorageEntry) error + Delete(string) error +} + +// StorageEntry is the entry for an item in a Storage implementation. +type StorageEntry struct { + Key string + Value []byte +} diff --git a/vault/barrier.go b/vault/barrier.go index 7f4fd5992..7c2888a37 100644 --- a/vault/barrier.go +++ b/vault/barrier.go @@ -1,6 +1,10 @@ package vault -import "errors" +import ( + "errors" + + "github.com/hashicorp/vault/logical" +) var ( // ErrBarrierSealed is returned if an operation is performed on @@ -76,3 +80,11 @@ type Entry struct { Key string Value []byte } + +// Logical turns the Entry into a logical storage entry. +func (e *Entry) Logical() *logical.StorageEntry { + return &logical.StorageEntry{ + Key: e.Key, + Value: e.Value, + } +} diff --git a/vault/barrier_view.go b/vault/barrier_view.go index 7dfe0b7bb..0f3940e5f 100644 --- a/vault/barrier_view.go +++ b/vault/barrier_view.go @@ -1,12 +1,18 @@ package vault -import "strings" +import ( + "strings" -// BarrierView is used to wrap a barrier and ensure all access is automatically -// prefixed. This means that nothing outside of the given prefix can be -// accessed through the view, which is an additional layer of security when -// interacting with the security barrier. Conceptually this is like a -// "chroot" into the barrier. + "github.com/hashicorp/vault/logical" +) + +// BarrierView wraps a SecurityBarrier and ensures all access is automatically +// prefixed. This is used to prevent anyone with access to the view to access +// any data in the durable storage outside of their prefix. Conceptually this +// is like a "chroot" into the barrier. +// +// BarrierView implements logical.Storage so it can be passed in as the +// durable storage mechanism for logical views. type BarrierView struct { barrier SecurityBarrier prefix string @@ -21,14 +27,32 @@ func NewBarrierView(barrier SecurityBarrier, prefix string) *BarrierView { } } -// SubView constructs a nested sub-view using the given prefix -func (v *BarrierView) SubView(prefix string) *BarrierView { - sub := v.expandKey(prefix) - return &BarrierView{barrier: v.barrier, prefix: sub} +// logical.Storage impl. +func (v *BarrierView) List(prefix string) ([]string, error) { + return v.barrier.List(v.expandKey(prefix)) } -// Put is used to insert or update an entry -func (v *BarrierView) Put(entry *Entry) error { +// logical.Storage impl. +func (v *BarrierView) Get(key string) (*logical.StorageEntry, error) { + entry, err := v.barrier.Get(v.expandKey(key)) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + if entry != nil { + entry.Key = v.truncateKey(entry.Key) + } + + return &logical.StorageEntry{ + Key: entry.Key, + Value: entry.Value, + }, nil +} + +// logical.Storage impl. +func (v *BarrierView) Put(entry *logical.StorageEntry) error { nested := &Entry{ Key: v.expandKey(entry.Key), Value: entry.Value, @@ -36,27 +60,15 @@ func (v *BarrierView) Put(entry *Entry) error { return v.barrier.Put(nested) } -// Get is used to fetch an entry -func (v *BarrierView) Get(key string) (*Entry, error) { - entry, err := v.barrier.Get(v.expandKey(key)) - if err != nil { - return nil, err - } - if entry != nil { - entry.Key = v.truncateKey(entry.Key) - } - return entry, nil -} - -// Delete is used to permanently delete an entry +// logical.Storage impl. func (v *BarrierView) Delete(key string) error { return v.barrier.Delete(v.expandKey(key)) } -// List is used ot list all the keys under a given -// prefix, up to the next prefix. -func (v *BarrierView) List(prefix string) ([]string, error) { - return v.barrier.List(v.expandKey(prefix)) +// SubView constructs a nested sub-view using the given prefix +func (v *BarrierView) SubView(prefix string) *BarrierView { + sub := v.expandKey(prefix) + return &BarrierView{barrier: v.barrier, prefix: sub} } // expandKey is used to expand to the full key path with the prefix diff --git a/vault/barrier_view_test.go b/vault/barrier_view_test.go index f272d1dfd..e01a95c87 100644 --- a/vault/barrier_view_test.go +++ b/vault/barrier_view_test.go @@ -1,6 +1,14 @@ package vault -import "testing" +import ( + "testing" + + "github.com/hashicorp/vault/logical" +) + +func TestBarrierView_impl(t *testing.T) { + var _ logical.Storage = new(BarrierView) +} func TestBarrierView(t *testing.T) { _, barrier, _ := mockBarrier(t) @@ -31,16 +39,16 @@ func TestBarrierView(t *testing.T) { } // Try to put the same entry via the view - if err := view.Put(entry); err != nil { + if err := view.Put(entry.Logical()); err != nil { t.Fatalf("err: %v", err) } // Check it is nested - out, err = barrier.Get("foo/test") + entry, err = barrier.Get("foo/test") if err != nil { t.Fatalf("err: %v", err) } - if out == nil { + if entry == nil { t.Fatalf("missing nested foo/test") } @@ -50,20 +58,20 @@ func TestBarrierView(t *testing.T) { } // Check the nested key - out, err = barrier.Get("foo/test") + entry, err = barrier.Get("foo/test") if err != nil { t.Fatalf("err: %v", err) } - if out != nil { + if entry != nil { t.Fatalf("nested foo/test should be gone") } // Check the non-nested key - out, err = barrier.Get("test") + entry, err = barrier.Get("test") if err != nil { t.Fatalf("err: %v", err) } - if out == nil { + if entry == nil { t.Fatalf("root test missing") } } @@ -92,17 +100,17 @@ func TestBarrierView_SubView(t *testing.T) { } // Try to put the same entry via the view - entry := &Entry{Key: "test", Value: []byte("test")} + entry := &logical.StorageEntry{Key: "test", Value: []byte("test")} if err := view.Put(entry); err != nil { t.Fatalf("err: %v", err) } // Check it is nested - out, err = barrier.Get("foo/bar/test") + bout, err := barrier.Get("foo/bar/test") if err != nil { t.Fatalf("err: %v", err) } - if out == nil { + if bout == nil { t.Fatalf("missing nested foo/bar/test") } @@ -121,11 +129,11 @@ func TestBarrierView_SubView(t *testing.T) { } // Check the nested key - out, err = barrier.Get("foo/bar/test") + bout, err = barrier.Get("foo/bar/test") if err != nil { t.Fatalf("err: %v", err) } - if out != nil { + if bout != nil { t.Fatalf("nested foo/bar/test should be gone") } } diff --git a/vault/expiration.go b/vault/expiration.go index 3f1ec1ebb..c70edba15 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -6,6 +6,8 @@ import ( "path" "sync" "time" + + "github.com/hashicorp/vault/logical" ) const ( @@ -173,7 +175,7 @@ func (m *ExpirationManager) Register(req *Request, resp *Response) (string, erro } // Write out to the view - ent := Entry{ + ent := logical.StorageEntry{ Key: le.VaultID, Value: buf, } diff --git a/vault/generic.go b/vault/generic.go index feb70ebf1..b1e5fb37f 100644 --- a/vault/generic.go +++ b/vault/generic.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "time" + + "github.com/hashicorp/vault/logical" ) // GenericBackend is used for the storing generic secrets. These are not @@ -99,7 +101,7 @@ func (g *GenericBackend) handleWrite(req *Request) (*Response, error) { } // Write out a new key - entry := &Entry{ + entry := &logical.StorageEntry{ Key: req.Path, Value: buf, }