diff --git a/http/logical_test.go b/http/logical_test.go index 1a2cdd065fba..bbbd89296654 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -152,6 +152,7 @@ func TestLogical_StandbyRedirect(t *testing.T) { "ttl": json.Number("0"), "creation_ttl": json.Number("0"), "explicit_max_ttl": json.Number("0"), + "expire_time": nil, }, "warnings": nilWarnings, "wrap_info": nil, diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index ea9f8d98ad86..41cb2a540c51 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -312,6 +312,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) { "ttl": json.Number("0"), "path": "auth/token/root", "explicit_max_ttl": json.Number("0"), + "expire_time": nil, } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") @@ -401,6 +402,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { "ttl": json.Number("0"), "path": "auth/token/root", "explicit_max_ttl": json.Number("0"), + "expire_time": nil, } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") diff --git a/vault/expiration.go b/vault/expiration.go index 87fceaba6755..f0f885edbd81 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -395,7 +395,7 @@ func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*log } // Check if the lease is renewable - if err := le.renewable(); err != nil { + if _, err := le.renewable(); err != nil { return nil, err } @@ -450,7 +450,7 @@ func (m *ExpirationManager) RenewToken(req *logical.Request, source string, toke // Check if the lease is renewable. Note that this also checks for a nil // lease and errors in that case as well. - if err := le.renewable(); err != nil { + if _, err := le.renewable(); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } @@ -841,29 +841,34 @@ type leaseEntry struct { } // encode is used to JSON encode the lease entry -func (l *leaseEntry) encode() ([]byte, error) { - return json.Marshal(l) +func (le *leaseEntry) encode() ([]byte, error) { + return json.Marshal(le) } -func (le *leaseEntry) renewable() error { +func (le *leaseEntry) renewable() (bool, error) { + var err error + switch { // If there is no entry, cannot review - if le == nil || le.ExpireTime.IsZero() { - return fmt.Errorf("lease not found or lease is not renewable") - } - + case le == nil || le.ExpireTime.IsZero(): + err = fmt.Errorf("lease not found or lease is not renewable") // Determine if the lease is expired - if le.ExpireTime.Before(time.Now()) { - return fmt.Errorf("lease expired") - } - + case le.ExpireTime.Before(time.Now()): + err = fmt.Errorf("lease expired") // Determine if the lease is renewable - if le.Secret != nil && !le.Secret.Renewable { - return fmt.Errorf("lease is not renewable") + case le.Secret != nil && !le.Secret.Renewable: + err = fmt.Errorf("lease is not renewable") + case le.Auth != nil && !le.Auth.Renewable: + err = fmt.Errorf("lease is not renewable") } - if le.Auth != nil && !le.Auth.Renewable { - return fmt.Errorf("lease is not renewable") + + if err != nil { + return false, err } - return nil + return true, nil +} + +func (le *leaseEntry) ttl() int64 { + return int64(le.ExpireTime.Sub(time.Now().Round(time.Second)).Seconds()) } // decodeLeaseEntry is used to reverse encode and return a new entry diff --git a/vault/expiration_test.go b/vault/expiration_test.go index b8255b3e9d6b..ced6b42318f7 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -1075,7 +1075,8 @@ func TestLeaseEntry(t *testing.T) { }, Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ - TTL: time.Minute, + TTL: time.Minute, + Renewable: true, }, }, IssueTime: time.Now(), @@ -1095,6 +1096,37 @@ func TestLeaseEntry(t *testing.T) { if !reflect.DeepEqual(out.Data, le.Data) { t.Fatalf("got: %#v, expect %#v", out, le) } + + // Test renewability + le.ExpireTime = time.Time{} + if r, _ := le.renewable(); r { + t.Fatal("lease with zero expire time is not renewable") + } + le.ExpireTime = time.Now().Add(-1 * time.Hour) + if r, _ := le.renewable(); r { + t.Fatal("lease with expire time in the past is not renewable") + } + le.ExpireTime = time.Now().Add(1 * time.Hour) + if r, err := le.renewable(); !r { + t.Fatalf("lease with future expire time is renewable, err: %v", err) + } + le.Secret.LeaseOptions.Renewable = false + if r, _ := le.renewable(); r { + t.Fatal("secret is set to not be renewable but returns as renewable") + } + le.Secret = nil + le.Auth = &logical.Auth{ + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + }, + } + if r, err := le.renewable(); !r { + t.Fatalf("auth is renewable but is set to not be, err: %v", err) + } + le.Auth.LeaseOptions.Renewable = false + if r, _ := le.renewable(); r { + t.Fatal("auth is set to not be renewable but returns as renewable") + } } func TestExpiration_RevokeForce(t *testing.T) { diff --git a/vault/logical_system.go b/vault/logical_system.go index a125bbf5a8f2..5fdf3122e8bc 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -55,7 +55,6 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen Root: []string{ "auth/*", "remount", - "revoke-prefix/*", "audit", "audit/*", "raw/*", @@ -63,6 +62,10 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "replication/reindex", "rotate", "config/auditing/*", + "revoke-prefix/*", + "leases/revoke-prefix/*", + "leases/revoke-force/*", + "leases/lookup/*", }, Unauthenticated: []string{ @@ -299,7 +302,43 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "renew" + framework.OptionalParamRegex("url_lease_id"), + Pattern: "leases/lookup/(?P.+?)?", + + Fields: map[string]*framework.FieldSchema{ + "prefix": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["leases-list-prefix"][0]), + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.handleLeaseLookupList, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["leases"][0]), + HelpDescription: strings.TrimSpace(sysHelp["leases"][1]), + }, + + &framework.Path{ + Pattern: "leases/lookup", + + Fields: map[string]*framework.FieldSchema{ + "lease_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["lease_id"][0]), + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleLeaseLookup, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["leases"][0]), + HelpDescription: strings.TrimSpace(sysHelp["leases"][1]), + }, + + &framework.Path{ + Pattern: "(leases/)?renew" + framework.OptionalParamRegex("url_lease_id"), Fields: map[string]*framework.FieldSchema{ "url_lease_id": &framework.FieldSchema{ @@ -325,7 +364,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke" + framework.OptionalParamRegex("url_lease_id"), + Pattern: "(leases/)?revoke" + framework.OptionalParamRegex("url_lease_id"), Fields: map[string]*framework.FieldSchema{ "url_lease_id": &framework.FieldSchema{ @@ -347,7 +386,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke-force/(?P.+)", + Pattern: "(leases/)?revoke-force/(?P.+)", Fields: map[string]*framework.FieldSchema{ "prefix": &framework.FieldSchema{ @@ -365,7 +404,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke-prefix/(?P.+)", + Pattern: "(leases/)?revoke-prefix/(?P.+)", Fields: map[string]*framework.FieldSchema{ "prefix": &framework.FieldSchema{ @@ -686,6 +725,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen HelpSynopsis: strings.TrimSpace(sysHelp["audited-headers-name"][0]), HelpDescription: strings.TrimSpace(sysHelp["audited-headers-name"][1]), }, + &framework.Path{ Pattern: "config/auditing/request-headers$", @@ -1274,6 +1314,61 @@ func (b *SystemBackend) handleTuneWriteCommon( return nil, nil } +// handleLease is use to view the metadata for a given LeaseID +func (b *SystemBackend) handleLeaseLookup( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + leaseID := data.Get("lease_id").(string) + if leaseID == "" { + return logical.ErrorResponse("lease_id must be specified"), + logical.ErrInvalidRequest + } + + leaseTimes, err := b.Core.expiration.FetchLeaseTimes(leaseID) + if err != nil { + b.Backend.Logger().Error("sys: error retrieving lease", "lease_id", leaseID, "error", err) + return handleError(err) + } + if leaseTimes == nil { + return logical.ErrorResponse("invalid lease"), logical.ErrInvalidRequest + } + + resp := &logical.Response{ + Data: map[string]interface{}{ + "id": leaseID, + "issue_time": leaseTimes.IssueTime, + "expire_time": nil, + "last_renewal": nil, + "ttl": int64(0), + }, + } + renewable, _ := leaseTimes.renewable() + resp.Data["renewable"] = renewable + + if !leaseTimes.LastRenewalTime.IsZero() { + resp.Data["last_renewal"] = leaseTimes.LastRenewalTime + } + if !leaseTimes.ExpireTime.IsZero() { + resp.Data["expire_time"] = leaseTimes.ExpireTime + resp.Data["ttl"] = leaseTimes.ttl() + } + return resp, nil +} + +func (b *SystemBackend) handleLeaseLookupList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + prefix := data.Get("prefix").(string) + if prefix != "" && !strings.HasSuffix(prefix, "/") { + prefix = prefix + "/" + } + + keys, err := b.Core.expiration.idView.List(prefix) + if err != nil { + b.Backend.Logger().Error("sys: error listing leases", "prefix", prefix, "error", err) + return handleError(err) + } + return logical.ListResponse(keys), nil +} + // handleRenew is used to renew a lease with a given LeaseID func (b *SystemBackend) handleRenew( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -2429,4 +2524,22 @@ This path responds to the following HTTP methods. "Lists the headers configured to be audited.", `Returns a list of headers that have been configured to be audited.`, }, + + "leases": { + `View or list lease metadata.`, + ` +This path responds to the following HTTP methods. + + PUT / + Retrieve the metadata for the provided lease id. + + LIST / + Lists the leases for the named prefix. + `, + }, + + "leases-list-prefix": { + `The path to list leases under. Example: "aws/creds/deploy"`, + "", + }, } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 67cdc8c827c1..4f3f70fc420c 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -11,13 +11,13 @@ import ( "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/logical" + "github.com/mitchellh/mapstructure" ) func TestSystemBackend_RootPaths(t *testing.T) { expected := []string{ "auth/*", "remount", - "revoke-prefix/*", "audit", "audit/*", "raw/*", @@ -25,6 +25,10 @@ func TestSystemBackend_RootPaths(t *testing.T) { "replication/reindex", "rotate", "config/auditing/*", + "revoke-prefix/*", + "leases/revoke-prefix/*", + "leases/revoke-force/*", + "leases/lookup/*", } b := testSystemBackend(t) @@ -315,6 +319,196 @@ func TestSystemBackend_remount_system(t *testing.T) { } } +func TestSystemBackend_leases(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // Read lease + req = logical.TestRequest(t, logical.UpdateOperation, "leases/lookup") + req.Data["lease_id"] = resp.Secret.LeaseID + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Data["renewable"] == nil || resp.Data["renewable"].(bool) { + t.Fatal("generic leases are not renewable") + } + + // Invalid lease + req = logical.TestRequest(t, logical.UpdateOperation, "leases/lookup") + req.Data["lease_id"] = "invalid" + resp, err = b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("expected invalid request, got err: %v", err) + } +} + +func TestSystemBackend_leases_list(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // List top level + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + var keys []string + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 1 { + t.Fatalf("Expected 1 subkey lease, got %d: %#v", len(keys), keys) + } + if keys[0] != "secret/" { + t.Fatal("Expected only secret subkey") + } + + // List lease + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret/foo") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 1 { + t.Fatalf("Expected 1 secret lease, got %d: %#v", len(keys), keys) + } + + // Generate multiple leases + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret/foo") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 3 { + t.Fatalf("Expected 3 secret lease, got %d: %#v", len(keys), keys) + } + + // Listing subkeys + req = logical.TestRequest(t, logical.UpdateOperation, "secret/bar") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/bar") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 2 { + t.Fatalf("Expected 2 secret lease, got %d: %#v", len(keys), keys) + } + expected := []string{"bar/", "foo/"} + if !reflect.DeepEqual(expected, keys) { + t.Fatalf("exp: %#v, act: %#v", expected, keys) + } +} + func TestSystemBackend_renew(t *testing.T) { core, b, root := testCoreSystemBackend(t) @@ -342,7 +536,7 @@ func TestSystemBackend_renew(t *testing.T) { } // Attempt renew - req2 := logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID) + req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) resp2, err := b.HandleRequest(req2) if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) @@ -378,7 +572,7 @@ func TestSystemBackend_renew(t *testing.T) { } // Attempt renew - req2 = logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID) + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) resp2, err = b.HandleRequest(req2) if err != nil { t.Fatalf("err: %v", err) @@ -394,6 +588,23 @@ func TestSystemBackend_renew(t *testing.T) { } // Test the other route path + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/renew") + req2.Data["lease_id"] = resp.Secret.LeaseID + resp2, err = b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp2.IsError() { + t.Fatalf("got an error") + } + if resp2.Data == nil { + t.Fatal("nil data") + } + if resp.Secret.TTL != 180*time.Second { + t.Fatalf("bad lease duration: %v", resp.Secret.TTL) + } + + // Test orig path req2 = logical.TestRequest(t, logical.UpdateOperation, "renew") req2.Data["lease_id"] = resp.Secret.LeaseID resp2, err = b.HandleRequest(req2) @@ -414,6 +625,31 @@ func TestSystemBackend_renew(t *testing.T) { func TestSystemBackend_renew_invalidID(t *testing.T) { b := testSystemBackend(t) + // Attempt renew + req := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/foobarbaz") + resp, err := b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } + + // Attempt renew with other method + req = logical.TestRequest(t, logical.UpdateOperation, "leases/renew") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) { + b := testSystemBackend(t) + // Attempt renew req := logical.TestRequest(t, logical.UpdateOperation, "renew/foobarbaz") resp, err := b.HandleRequest(req) @@ -504,11 +740,58 @@ func TestSystemBackend_revoke(t *testing.T) { if resp2 != nil { t.Fatalf("bad: %#v", resp) } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // Test the other route path + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke") + req2.Data["lease_id"] = resp.Secret.LeaseID + resp2, err = b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v %#v", err, resp2) + } + if resp2 != nil { + t.Fatalf("bad: %#v", resp) + } } func TestSystemBackend_revoke_invalidID(t *testing.T) { b := testSystemBackend(t) + // Attempt revoke + req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke/foobarbaz") + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + // Attempt revoke with other method + req = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_revoke_invalidID_origUrl(t *testing.T) { + b := testSystemBackend(t) + // Attempt revoke req := logical.TestRequest(t, logical.UpdateOperation, "revoke/foobarbaz") resp, err := b.HandleRequest(req) @@ -558,6 +841,54 @@ func TestSystemBackend_revokePrefix(t *testing.T) { t.Fatalf("bad: %#v", resp) } + // Attempt revoke + req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/secret/") + resp2, err := b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v %#v", err, resp2) + } + if resp2 != nil { + t.Fatalf("bad: %#v", resp) + } + + // Attempt renew + req3 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) + resp3, err := b.HandleRequest(req3) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp3.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_revokePrefix_origUrl(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.Data["lease"] = "1h" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + // Attempt revoke req2 := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/secret/") resp2, err := b.HandleRequest(req2) @@ -624,6 +955,69 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) { t.Fatalf("err: %v", err) } + req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/auth/github/") + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + te, err = ts.Lookup(te.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if te != nil { + t.Fatalf("bad: %v", te) + } +} + +func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) { + core, ts, _, _ := TestCoreWithTokenStore(t) + bc := &logical.BackendConfig{ + Logger: core.logger, + System: logical.StaticSystemView{ + DefaultLeaseTTLVal: time.Hour * 24, + MaxLeaseTTLVal: time.Hour * 24 * 32, + }, + } + b, err := NewSystemBackend(core, bc) + if err != nil { + t.Fatal(err) + } + + exp := ts.expiration + + te := &TokenEntry{ + ID: "foo", + Path: "auth/github/login/bar", + } + err = ts.create(te) + if err != nil { + t.Fatal(err) + } + + te, err = ts.Lookup("foo") + if err != nil { + t.Fatal(err) + } + if te == nil { + t.Fatal("token entry was nil") + } + + // Create a new token + auth := &logical.Auth{ + ClientToken: te.ID, + LeaseOptions: logical.LeaseOptions{ + TTL: time.Hour, + }, + } + err = exp.RegisterAuth(te.Path, auth) + if err != nil { + t.Fatalf("err: %v", err) + } + req := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/auth/github/") resp, err := b.HandleRequest(req) if err != nil { diff --git a/vault/token_store.go b/vault/token_store.go index 4bd27ebfae03..384d67f86726 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -1858,6 +1858,7 @@ func (ts *TokenStore) handleLookup( "orphan": false, "creation_time": int64(out.CreationTime), "creation_ttl": int64(out.TTL.Seconds()), + "expire_time": nil, "ttl": int64(0), "explicit_max_ttl": int64(out.ExplicitMaxTTL.Seconds()), }, @@ -1882,15 +1883,15 @@ func (ts *TokenStore) handleLookup( if leaseTimes != nil { if !leaseTimes.LastRenewalTime.IsZero() { resp.Data["last_renewal_time"] = leaseTimes.LastRenewalTime.Unix() + resp.Data["last_renewal"] = leaseTimes.LastRenewalTime } if !leaseTimes.ExpireTime.IsZero() { - resp.Data["ttl"] = int64(leaseTimes.ExpireTime.Sub(time.Now().Round(time.Second)).Seconds()) - } - if err := leaseTimes.renewable(); err == nil { - resp.Data["renewable"] = true - } else { - resp.Data["renewable"] = false + resp.Data["expire_time"] = leaseTimes.ExpireTime + resp.Data["ttl"] = leaseTimes.ttl() } + renewable, _ := leaseTimes.renewable() + resp.Data["renewable"] = renewable + resp.Data["issue_time"] = leaseTimes.IssueTime } if urltoken { diff --git a/vault/token_store_test.go b/vault/token_store_test.go index d94c329e2bdf..7a84fe737f2c 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -1358,6 +1358,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { "creation_ttl": int64(0), "ttl": int64(0), "explicit_max_ttl": int64(0), + "expire_time": nil, } if resp.Data["creation_time"].(int64) == 0 { @@ -1403,6 +1404,14 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatal("issue time is default time") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatal("expire time is default time") + } + delete(resp.Data, "expire_time") // Depending on timing of the test this may have ticked down, so accept 3599 if resp.Data["ttl"].(int64) == 3599 { @@ -1445,6 +1454,14 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatal("issue time is default time") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatal("expire time is default time") + } + delete(resp.Data, "expire_time") // Depending on timing of the test this may have ticked down, so accept 3599 if resp.Data["ttl"].(int64) == 3599 { @@ -1486,9 +1503,11 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { - _, ts, _, root := TestCoreWithTokenStore(t) + c, ts, _, root := TestCoreWithTokenStore(t) + testCoreMakeToken(t, c, root, "client", "3600s", []string{"foo"}) + req := logical.TestRequest(t, logical.ReadOperation, "lookup-self") - req.ClientToken = root + req.ClientToken = "client" resp, err := ts.HandleRequest(req) if err != nil { t.Fatalf("err: %v %v", err, resp) @@ -1498,16 +1517,17 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { } exp := map[string]interface{}{ - "id": root, + "id": "client", "accessor": resp.Data["accessor"], - "policies": []string{"root"}, - "path": "auth/token/root", + "policies": []string{"default", "foo"}, + "path": "auth/token/create", "meta": map[string]string(nil), - "display_name": "root", - "orphan": true, + "display_name": "token", + "orphan": false, + "renewable": true, "num_uses": 0, - "creation_ttl": int64(0), - "ttl": int64(0), + "creation_ttl": int64(3600), + "ttl": int64(3600), "explicit_max_ttl": int64(0), } @@ -1515,6 +1535,19 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatalf("creation time was zero") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatalf("expire time was zero") + } + delete(resp.Data, "expire_time") + + // Depending on timing of the test this may have ticked down, so accept 3599 + if resp.Data["ttl"].(int64) == 3599 { + resp.Data["ttl"] = int64(3600) + } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("bad: expected:%#v\nactual:%#v", exp, resp.Data) diff --git a/website/source/api/system/leases.html.md b/website/source/api/system/leases.html.md new file mode 100644 index 000000000000..ec5166479a74 --- /dev/null +++ b/website/source/api/system/leases.html.md @@ -0,0 +1,222 @@ +--- +layout: "api" +page_title: "/sys/leases - HTTP API" +sidebar_current: "docs-http-system-leases" +description: |- + The `/sys/leases` endpoints are used to view and manage leases. +--- + +# `/sys/leases` + +The `/sys/leases` endpoints are used to view and manage leases in Vault. + +## Read Lease + +This endpoint retrieve lease metadata. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/lookup` | `200 application/json` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to lookup. + +### Sample Payload + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234..." +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/lookup +``` + +### Sample Response + +```json +{ + "id": "auth/token/create/25c75065466dfc5f920525feafe47502c4c9915c", + "issue_time": "2017-04-30T10:18:11.228946471-04:00", + "expire_time": "2017-04-30T11:18:11.228946708-04:00", + "last_renewal_time": null, + "renewable": true, + "ttl": 3558 +} +``` + +## List Leases + +This endpoint returns a list of lease ids. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `LIST` | `/sys/leases/lookup/:prefix` | `200 application/json` | + + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + https://vault.rocks/v1/sys/leases/lookup/aws/creds/deploy/ +``` + +### Sample Response + +```json +{ + "data":{ + "keys":[ + "abcd-1234...", + "efgh-1234...", + "ijkl-1234..." + ] + } +} +``` + +## Renew Lease + +This endpoint renews a lease, requesting to extend the lease. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/renew` | `200 application/json` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to extend. + This can be specified as part of the URL or as part of the request body. + +- `increment` `(int: 0)` – Specifies the requested amount of time (in seconds) + to extend the lease. + +### Sample Payload + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234...", + "increment": 1800 +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/renew +``` + +### Sample Response + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234...", + "renewable": true, + "lease_duration": 2764790 +} +``` + +## Revoke Lease + +This endpoint revokes a lease immediately. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke` | `204 (empty body)` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to revoke. + +### Sample Payload + +```json +{ + "lease_id": "postgresql/creds/readonly/abcd-1234..." +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/revoke +``` + +## Revoke Force + +This endpoint revokes all secrets or tokens generated under a given prefix +immediately. Unlike `/sys/leases/revoke-prefix`, this path ignores backend errors +encountered during revocation. This is _potentially very dangerous_ and should +only be used in specific emergency situations where errors in the backend or the +connected backend service prevent normal revocation. + +By ignoring these errors, Vault abdicates responsibility for ensuring that the +issued credentials or secrets are properly revoked and/or cleaned up. Access to +this endpoint should be tightly controlled. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :---------------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke-force/:prefix` | `204 (empty body)` | + +### Parameters + +- `prefix` `(string: )` – Specifies the prefix to revoke. This is + specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + https://vault.rocks/v1/sys/leases/revoke-force/aws/creds +``` + +## Revoke Prefix + +This endpoint revokes all secrets (via a lease ID prefix) or tokens (via the +tokens' path property) generated under a given prefix immediately. This requires +`sudo` capability and access to it should be tightly controlled as it can be +used to revoke very large numbers of secrets/tokens at once. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :---------------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke-prefix/:prefix` | `204 (empty body)` | + +### Parameters + +- `prefix` `(string: )` – Specifies the prefix to revoke. This is + specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + https://vault.rocks/v1/sys/leases/revoke-prefix/aws/creds +``` diff --git a/website/source/api/system/renew.html.md b/website/source/api/system/renew.html.md deleted file mode 100644 index 3b67663ac3b7..000000000000 --- a/website/source/api/system/renew.html.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -layout: "api" -page_title: "/sys/renew - HTTP API" -sidebar_current: "docs-http-system-renew" -description: |- - The `/sys/renew` endpoint is used to renew secrets. ---- - -# `/sys/renew` - -The `/sys/renew` endpoint is used to renew secrets. - -## Renew Secret - -This endpoint renews a secret, requesting to extend the lease. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/renew` | `200 application/json` | - -### Parameters - -- `lease_id` `(string: )` – Specifies the ID of the lease to extend. - This can be specified as part of the URL or as part of the request body. - -- `increment` `(int: 0)` – Specifies the requested amount of time (in seconds) - to extend the lease. - -### Sample Payload - -```json -{ - "lease_id": "aws/creds/deploy/abcd-1234...", - "increment": 1800 -} -``` - -### Sample Request - -With the `lease_id` in the request body: - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - --data @payload.json \ - https://vault.rocks/v1/sys/renew -``` - -### Sample Response - -```json -{ - "lease_id": "aws/creds/deploy/abcd-1234...", - "renewable": true, - "lease_duration": 2764790 -} -``` diff --git a/website/source/api/system/revoke-force.html.md b/website/source/api/system/revoke-force.html.md deleted file mode 100644 index 5ae915127224..000000000000 --- a/website/source/api/system/revoke-force.html.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke-force - HTTP API" -sidebar_current: "docs-http-system-revoke-force" -description: |- - The `/sys/revoke-force` endpoint is used to revoke secrets or tokens based on - prefix while ignoring backend errors. ---- - -# `/sys/revoke-force` - -The `/sys/revoke-force` endpoint is used to revoke secrets or tokens based on -prefix while ignoring backend errors. - -## Revoke Force - -This endpoint revokes all secrets or tokens generated under a given prefix -immediately. Unlike `/sys/revoke-prefix`, this path ignores backend errors -encountered during revocation. This is _potentially very dangerous_ and should -only be used in specific emergency situations where errors in the backend or the -connected backend service prevent normal revocation. - -By ignoring these errors, Vault abdicates responsibility for ensuring that the -issued credentials or secrets are properly revoked and/or cleaned up. Access to -this endpoint should be tightly controlled. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke-force/:prefix` | `204 (empty body)` | - -### Parameters - -- `prefix` `(string: )` – Specifies the prefix to revoke. This is - specified as part of the URL. - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - https://vault.rocks/v1/sys/revoke-force/aws/creds -``` diff --git a/website/source/api/system/revoke-prefix.html.md b/website/source/api/system/revoke-prefix.html.md deleted file mode 100644 index 8ebc81c3e275..000000000000 --- a/website/source/api/system/revoke-prefix.html.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke-prefix - HTTP API" -sidebar_current: "docs-http-system-revoke-prefix" -description: |- - The `/sys/revoke-prefix` endpoint is used to revoke secrets or tokens based on - prefix. ---- - -# `/sys/revoke-prefix` - -The `/sys/revoke-prefix` endpoint is used to revoke secrets or tokens based on -prefix. - -## Revoke Prefix - -This endpoint revokes all secrets (via a lease ID prefix) or tokens (via the -tokens' path property) generated under a given prefix immediately. This requires -`sudo` capability and access to it should be tightly controlled as it can be -used to revoke very large numbers of secrets/tokens at once. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke-prefix/:prefix` | `204 (empty body)` | - -### Parameters - -- `prefix` `(string: )` – Specifies the prefix to revoke. This is - specified as part of the URL. - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - https://vault.rocks/v1/sys/revoke-prefix/aws/creds -``` diff --git a/website/source/api/system/revoke.html.md b/website/source/api/system/revoke.html.md deleted file mode 100644 index 7009c809f337..000000000000 --- a/website/source/api/system/revoke.html.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke - HTTP API" -sidebar_current: "docs-http-system-revoke/" -description: |- - The `/sys/revoke` endpoint is used to revoke secrets. ---- - -# `/sys/revoke` - -The `/sys/revoke` endpoint is used to revoke secrets. - -## Revoke Secret - -This endpoint revokes a secret immediately. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke` | `204 (empty body)` | - -### Parameters - -- `lease_id` `(string: )` – Specifies the ID of the lease to revoke. - -### Sample Payload - -```json -{ - "lease_id": "postgresql/creds/readonly/abcd-1234..." -} -``` - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - --data @payload.json \ - https://vault.rocks/v1/sys/revoke -``` diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index c209937bc0bb..b4429a02bb26 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -98,6 +98,9 @@ > /sys/leader + > + /sys/leases + > /sys/mounts @@ -113,21 +116,9 @@ > /sys/remount - > - /sys/renew - > /sys/replication - > - /sys/revoke - - > - /sys/revoke-force - - > - /sys/revoke-prefix - > /sys/rotate