diff --git a/vault/expiration.go b/vault/expiration.go index 33348284e4..0330bb979c 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -1,6 +1,11 @@ package vault -import "time" +import ( + "encoding/json" + "fmt" + "path" + "time" +) const ( // expirationSubPath is the sub-path used for the expiration manager @@ -61,5 +66,68 @@ func (m *ExpirationManager) Renew(vaultID string, increment time.Duration) (*Lea // 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) { - return "", nil + // Ignore if there is no lease + if resp == nil || resp.Lease == nil { + return "", nil + } + + // Validate the lease + if err := resp.Lease.Validate(); err != nil { + return "", err + } + + // Cannot register a non-secret (e.g. a policy or configuration key) + if !resp.IsSecret { + return "", fmt.Errorf("cannot attach lease to non-secret") + } + + // Create a lease entry + le := leaseEntry{ + VaultID: path.Join(req.Path, generateUUID()), + Path: req.Path, + Data: resp.Data, + Lease: resp.Lease, + IssueTime: time.Now().UTC(), + } + + // Encode the entry + buf, err := le.encode() + if err != nil { + return "", fmt.Errorf("failed to encode lease entry: %v", err) + } + + // Write out to the view + ent := Entry{ + Key: le.VaultID, + Value: buf, + } + if err := m.view.Put(&ent); err != nil { + return "", fmt.Errorf("failed to persist lease entry: %v", err) + } + + // TODO: Automatic revoke timer... + + // Done + return le.VaultID, nil +} + +// leaseEntry is used to structure the values the expiration +// manager stores. This is used to handle renew and revocation. +type leaseEntry struct { + VaultID string + Path string + Data map[string]interface{} + Lease *Lease + IssueTime time.Time +} + +// encode is used to JSON encode the lease entry +func (l *leaseEntry) encode() ([]byte, error) { + return json.Marshal(l) +} + +// decodeLeaseEntry is used to reverse encode and return a new entry +func decodeLeaseEntry(buf []byte) (*leaseEntry, error) { + out := new(leaseEntry) + return out, json.Unmarshal(buf, out) } diff --git a/vault/expiration_test.go b/vault/expiration_test.go new file mode 100644 index 0000000000..6cec022764 --- /dev/null +++ b/vault/expiration_test.go @@ -0,0 +1,77 @@ +package vault + +import ( + "reflect" + "strings" + "testing" + "time" +) + +// mockExpiration returns a mock expiration manager +func mockExpiration(t *testing.T) *ExpirationManager { + router := NewRouter() + view := mockView(t, "expire/") + return NewExpirationManager(router, view) +} + +func TestExpiration_Register(t *testing.T) { + exp := mockExpiration(t) + req := &Request{ + Operation: ReadOperation, + Path: "prod/aws/foo", + } + resp := &Response{ + IsSecret: true, + Lease: &Lease{ + Duration: time.Hour, + MaxDuration: time.Hour, + }, + Data: map[string]interface{}{ + "access_key": "xyz", + "secret_key": "abcd", + }, + } + + id, err := exp.Register(req, resp) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !strings.HasPrefix(id, req.Path) { + t.Fatalf("bad: %s", id) + } + + if len(id) <= len(req.Path) { + t.Fatalf("bad: %s", id) + } +} + +func TestLeaseEntry(t *testing.T) { + le := &leaseEntry{ + VaultID: "foo/bar/1234", + Path: "foo/bar", + Data: map[string]interface{}{ + "testing": true, + }, + Lease: &Lease{ + Renewable: true, + Duration: time.Minute, + MaxDuration: time.Hour, + }, + IssueTime: time.Now(), + } + + enc, err := le.encode() + if err != nil { + t.Fatalf("err: %v", err) + } + + out, err := decodeLeaseEntry(enc) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !reflect.DeepEqual(out.Data, le.Data) { + t.Fatalf("got: %#v, expect %#v", out, le) + } +} diff --git a/vault/generic_test.go b/vault/generic_test.go index fb9f17eefe..7c8cadb3b4 100644 --- a/vault/generic_test.go +++ b/vault/generic_test.go @@ -7,8 +7,8 @@ import ( "github.com/hashicorp/vault/physical" ) -// mockRequest returns a request with a real view attached -func mockRequest(t *testing.T, op Operation, path string) *Request { +// 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 { @@ -21,7 +21,13 @@ func mockRequest(t *testing.T, op Operation, path string) *Request { b.Unseal(key) // Create the barrier view - view := NewBarrierView(b, "logical/") + 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{