diff --git a/vault/core.go b/vault/core.go index 92a43ff950..43c9eda2a8 100644 --- a/vault/core.go +++ b/vault/core.go @@ -282,18 +282,10 @@ func (c *Core) HandleLogin(req *credential.Request) (*credential.Response, error // Register with the expiration manager if there is a lease if resp.Secret.Lease > 0 { - lReq := &logical.Request{ - Path: req.Path, - Data: req.Data, - } - lResp := &logical.Response{ - Secret: resp.Secret, - Data: resp.Data, - } - vaultID, err := c.expiration.Register(lReq, lResp) + vaultID, err := c.expiration.RegisterLogin(te.ID, req, resp) if err != nil { c.logger.Printf( - "[ERR] core: failed to register lease "+ + "[ERR] core: failed to register login token lease "+ "(request: %#v, response: %#v): %v", req, resp, err) return nil, ErrInternalError } diff --git a/vault/expiration.go b/vault/expiration.go index 9b45f8455d..c29c602edf 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/hashicorp/vault/credential" "github.com/hashicorp/vault/logical" ) @@ -300,6 +301,50 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons return le.VaultID, nil } +// RegisterLogin is used to take a credential 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. This is distinct from Register +// as the behavior of renew and revocation differs a bit. +func (m *ExpirationManager) RegisterLogin(token string, req *credential.Request, resp *credential.Response) (string, error) { + // Ignore if there is no leased secret + if resp == nil || resp.Secret == nil || resp.Secret.Lease == 0 { + return "", nil + } + + // Validate the secret + if err := resp.Secret.Validate(); err != nil { + return "", err + } + + // Create a lease entry + now := time.Now().UTC() + leaseTotal := resp.Secret.Lease + resp.Secret.LeaseGracePeriod + le := leaseEntry{ + VaultID: path.Join(req.Path, generateUUID()), + LoginToken: token, + Path: req.Path, + Data: resp.Data, + Secret: resp.Secret, + IssueTime: now, + ExpireTime: now.Add(leaseTotal), + } + + // Encode the entry + if err := m.persistEntry(&le); err != nil { + return "", err + } + + // Setup revocation timer + m.pendingLock.Lock() + m.pending[le.VaultID] = time.AfterFunc(leaseTotal, func() { + m.expireID(le.VaultID) + }) + m.pendingLock.Unlock() + + // Done + return le.VaultID, nil +} + // expireID is invoked when a given ID is expired func (m *ExpirationManager) expireID(vaultID string) { // Clear from the pending expiration @@ -321,6 +366,16 @@ func (m *ExpirationManager) expireID(vaultID string) { // revokeEntry is used to attempt revocation of an internal entry func (m *ExpirationManager) revokeEntry(le *leaseEntry) error { + // Revocation of login tokens is special since we can by-pass the + // backend and directly interact with the token store + if le.LoginToken != "" { + if err := m.tokenStore.RevokeTree(le.LoginToken); err != nil { + return fmt.Errorf("failed to revoke token: %v", err) + } + return nil + } + + // Handle standard revocation via backends _, err := m.router.Route(logical.RevokeRequest( le.Path, le.Secret, le.Data)) if err != nil { @@ -390,6 +445,7 @@ func (m *ExpirationManager) deleteEntry(vaultID string) error { // manager stores. This is used to handle renew and revocation. type leaseEntry struct { VaultID string `json:"vault_id"` + LoginToken string `json:"login_token"` Path string `json:"path"` Data map[string]interface{} `json:"data"` Secret *logical.Secret `json:"secret"` diff --git a/vault/expiration_test.go b/vault/expiration_test.go index ed76a6ce40..9ae799cd1f 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/hashicorp/vault/credential" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/physical" ) @@ -126,6 +127,40 @@ func TestExpiration_Register(t *testing.T) { } } +func TestExpiration_RegisterLogin(t *testing.T) { + exp := mockExpiration(t) + root, err := exp.tokenStore.RootToken() + if err != nil { + t.Fatalf("err: %v", err) + } + + req := &credential.Request{ + Path: "auth/user/login", + } + resp := &credential.Response{ + Secret: &logical.Secret{ + Lease: time.Hour, + }, + Data: map[string]interface{}{ + "access_key": "xyz", + "secret_key": "abcd", + }, + } + + id, err := exp.RegisterLogin(root.ID, 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 TestExpiration_Revoke(t *testing.T) { exp := mockExpiration(t) noop := &NoopBackend{} @@ -407,6 +442,41 @@ func TestExpiration_revokeEntry(t *testing.T) { } } +func TestExpiration_revokeEntry_token(t *testing.T) { + exp := mockExpiration(t) + root, err := exp.tokenStore.RootToken() + if err != nil { + t.Fatalf("err: %v", err) + } + + le := &leaseEntry{ + VaultID: "foo/bar/1234", + LoginToken: root.ID, + Path: "foo/bar", + Data: map[string]interface{}{ + "testing": true, + }, + Secret: &logical.Secret{ + Lease: time.Minute, + }, + IssueTime: time.Now(), + ExpireTime: time.Now(), + } + + err = exp.revokeEntry(le) + if err != nil { + t.Fatalf("err: %v", err) + } + + out, err := exp.tokenStore.Lookup(root.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if out != nil { + t.Fatalf("bad: %v", out) + } +} + func TestExpiration_renewEntry(t *testing.T) { exp := mockExpiration(t)