diff --git a/logical/response.go b/logical/response.go index 2b491a4313..682374efae 100644 --- a/logical/response.go +++ b/logical/response.go @@ -66,3 +66,12 @@ func ErrorResponse(text string) *Response { }, } } + +// ListResponse is used to format a response to a list operation. +func ListResponse(keys []string) *Response { + return &Response{ + Data: map[string]interface{}{ + "keys": keys, + }, + } +} diff --git a/logical/storage_inmem.go b/logical/storage_inmem.go new file mode 100644 index 0000000000..c06f51a5e3 --- /dev/null +++ b/logical/storage_inmem.go @@ -0,0 +1,47 @@ +package logical + +import ( + "strings" + "sync" +) + +// InmemStorage implements Storage and stores all data in memory. +type InmemStorage struct { + Data map[string]*StorageEntry + + once sync.Once +} + +func (s *InmemStorage) List(prefix string) ([]string, error) { + s.once.Do(s.init) + + var result []string + for k, _ := range s.Data { + if strings.HasPrefix(k, prefix) { + result = append(result, k) + } + } + + return result, nil +} + +func (s *InmemStorage) Get(key string) (*StorageEntry, error) { + s.once.Do(s.init) + return s.Data[key], nil +} + +func (s *InmemStorage) Put(entry *StorageEntry) error { + s.once.Do(s.init) + s.Data[entry.Key] = entry + return nil +} + +func (s *InmemStorage) Delete(k string) error { + s.once.Do(s.init) + delete(s.Data, k) + return nil +} + +func (s *InmemStorage) init() { + s.Data = make(map[string]*StorageEntry) +} diff --git a/logical/storage_inmem_test.go b/logical/storage_inmem_test.go new file mode 100644 index 0000000000..8e0964fd4a --- /dev/null +++ b/logical/storage_inmem_test.go @@ -0,0 +1,9 @@ +package logical + +import ( + "testing" +) + +func TestInmemStorage(t *testing.T) { + TestStorage(t, new(InmemStorage)) +} diff --git a/logical/testing.go b/logical/testing.go new file mode 100644 index 0000000000..e4054d9287 --- /dev/null +++ b/logical/testing.go @@ -0,0 +1,61 @@ +package logical + +import ( + "reflect" + "testing" +) + +// TestRequest is a helper to create a purely in-memory Request struct. +func TestRequest(t *testing.T, op Operation, path string) *Request { + return &Request{ + Operation: op, + Path: path, + Data: make(map[string]interface{}), + Storage: new(InmemStorage), + } +} + +// TestStorage is a helper that can be used from unit tests to verify +// the behavior of a Storage impl. +func TestStorage(t *testing.T, s Storage) { + keys, err := s.List("") + if err != nil { + t.Fatalf("list error: %s", err) + } + if len(keys) > 0 { + t.Fatalf("should have no keys to start: %#v", keys) + } + + entry := &StorageEntry{Key: "foo", Value: []byte("bar")} + if err := s.Put(entry); err != nil { + t.Fatalf("put error: %s", err) + } + + actual, err := s.Get("foo") + if err != nil { + t.Fatalf("get error: %s", err) + } + if !reflect.DeepEqual(actual, entry) { + t.Fatalf("wrong value. Expected: %#v\nGot: %#v", entry, actual) + } + + keys, err = s.List("") + if err != nil { + t.Fatalf("list error: %s", err) + } + if !reflect.DeepEqual(keys, []string{"foo"}) { + t.Fatalf("bad keys: %#v", keys) + } + + if err := s.Delete("foo"); err != nil { + t.Fatalf("put error: %s", err) + } + + keys, err = s.List("") + if err != nil { + t.Fatalf("list error: %s", err) + } + if len(keys) > 0 { + t.Fatalf("should have no keys to start: %#v", keys) + } +} diff --git a/vault/barrier_view_test.go b/vault/barrier_view_test.go index e01a95c874..5b5ca69116 100644 --- a/vault/barrier_view_test.go +++ b/vault/barrier_view_test.go @@ -10,6 +10,12 @@ func TestBarrierView_impl(t *testing.T) { var _ logical.Storage = new(BarrierView) } +func TestBarrierView_spec(t *testing.T) { + _, barrier, _ := mockBarrier(t) + view := NewBarrierView(barrier, "foo/") + logical.TestStorage(t, view) +} + func TestBarrierView(t *testing.T) { _, barrier, _ := mockBarrier(t) view := NewBarrierView(barrier, "foo/") diff --git a/vault/logical_passthrough.go b/vault/logical_passthrough.go new file mode 100644 index 0000000000..628668d4b4 --- /dev/null +++ b/vault/logical_passthrough.go @@ -0,0 +1,123 @@ +package vault + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/hashicorp/vault/logical" +) + +// 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 +// fancy. +type PassthroughBackend struct{} + +func (b *PassthroughBackend) HandleRequest(req *logical.Request) (*logical.Response, error) { + // TODO(mitchellh): help, let's just do it when we migrate to helper/backend + + switch req.Operation { + case logical.ReadOperation: + return b.handleRead(req) + case logical.WriteOperation: + return b.handleWrite(req) + case logical.DeleteOperation: + return b.handleDelete(req) + case logical.ListOperation: + return b.handleList(req) + default: + return nil, ErrUnsupportedOperation + } +} + +func (b *PassthroughBackend) RootPaths() []string { + return nil +} + +func (b *PassthroughBackend) handleRead(req *logical.Request) (*logical.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 *logical.Lease + if ok { + leaseDuration, err := time.ParseDuration(leaseVal) + if err == nil { + lease = &logical.Lease{ + Renewable: false, + Revokable: false, + Duration: leaseDuration, + MaxDuration: leaseDuration, + MaxIncrement: 0, + } + } + } + + // Generate the response + resp := &logical.Response{ + IsSecret: true, + Lease: lease, + Data: raw, + } + return resp, nil +} + +func (b *PassthroughBackend) handleWrite(req *logical.Request) (*logical.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 (b *PassthroughBackend) handleDelete(req *logical.Request) (*logical.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 (b *PassthroughBackend) handleList(req *logical.Request) (*logical.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 + return logical.ListResponse(keys), nil +} diff --git a/vault/logical_passthrough_test.go b/vault/logical_passthrough_test.go new file mode 100644 index 0000000000..4ad084c417 --- /dev/null +++ b/vault/logical_passthrough_test.go @@ -0,0 +1,143 @@ +package vault + +import ( + "reflect" + "testing" + "time" + + "github.com/hashicorp/vault/logical" +) + +func TestPassthroughBackend_impl(t *testing.T) { + var _ logical.Backend = new(PassthroughBackend) +} + +func TestPassthroughBackend_RootPaths(t *testing.T) { + var b PassthroughBackend + root := b.RootPaths() + if len(root) != 0 { + t.Fatalf("unexpected: %v", root) + } +} + +func TestPassthroughBackend_Write(t *testing.T) { + var b PassthroughBackend + req := logical.TestRequest(t, logical.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 TestPassthroughBackend_Read(t *testing.T) { + var b PassthroughBackend + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.Data["raw"] = "test" + req.Data["lease"] = "1h" + storage := req.Storage + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storage + + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := &logical.Response{ + IsSecret: true, + Lease: &logical.Lease{ + Renewable: false, + Revokable: false, + Duration: time.Hour, + MaxDuration: time.Hour, + MaxIncrement: 0, + }, + Data: map[string]interface{}{ + "raw": "test", + "lease": "1h", + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } +} + +func TestPassthroughBackend_Delete(t *testing.T) { + var b PassthroughBackend + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.Data["raw"] = "test" + storage := req.Storage + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.DeleteOperation, "foo") + req.Storage = storage + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storage + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +func TestPassthroughBackend_List(t *testing.T) { + var b PassthroughBackend + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.Data["raw"] = "test" + storage := req.Storage + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ListOperation, "") + req.Storage = storage + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := &logical.Response{ + IsSecret: false, + Lease: nil, + Data: map[string]interface{}{ + "keys": []string{"foo"}, + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } +}