diff --git a/api/secret.go b/api/secret.go index bc52f78b72228..7e10f1ff0cea5 100644 --- a/api/secret.go +++ b/api/secret.go @@ -28,6 +28,7 @@ type Secret struct { // SecretAuth is the structure containing auth information if we have it. type SecretAuth struct { ClientToken string `json:"client_token"` + Accessor string `json:"accessor"` Policies []string `json:"policies"` Metadata map[string]string `json:"metadata"` diff --git a/command/format.go b/command/format.go index 0acac4b70a774..93b812312ecf2 100644 --- a/command/format.go +++ b/command/format.go @@ -143,6 +143,7 @@ func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error { if s.Auth != nil { input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken)) + input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor)) input = append(input, fmt.Sprintf("token_duration %s %d", config.Delim, s.Auth.LeaseDuration)) input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable)) input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies)) diff --git a/http/handler.go b/http/handler.go index 0a6a1081b56ff..67cff6af4cad0 100644 --- a/http/handler.go +++ b/http/handler.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/vault" ) @@ -34,6 +35,7 @@ func Handler(core *vault.Core) http.Handler { mux.Handle("/v1/sys/rekey/update", handleSysRekeyUpdate(core)) mux.Handle("/v1/sys/capabilities", handleSysCapabilities(core)) mux.Handle("/v1/sys/capabilities-self", handleSysCapabilities(core)) + mux.Handle("/v1/sys/capabilities-accessor", handleSysCapabilitiesAccessor(core)) mux.Handle("/v1/sys/", handleLogical(core, true)) mux.Handle("/v1/", handleLogical(core, false)) @@ -79,7 +81,7 @@ func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *l return resp, false } if err != nil { - respondError(w, http.StatusInternalServerError, err) + respondErrorStatus(w, err) return resp, false } @@ -139,6 +141,18 @@ func requestAuth(r *http.Request, req *logical.Request) *logical.Request { return req } +// Determines the type of the error being returned and sets the HTTP +// status code appropriately +func respondErrorStatus(w http.ResponseWriter, err error) { + status := http.StatusInternalServerError + switch { + // Keep adding more error types here to appropriate the status codes + case errwrap.ContainsType(err, new(vault.StatusBadRequest)): + status = http.StatusBadRequest + } + respondError(w, status, err) +} + func respondError(w http.ResponseWriter, status int, err error) { // Adjust status code when sealed if err == vault.ErrSealed { diff --git a/http/logical.go b/http/logical.go index eb07a8f6404dd..816df95c0c4ce 100644 --- a/http/logical.go +++ b/http/logical.go @@ -124,6 +124,7 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, dataOnl if resp.Auth != nil { logicalResp.Auth = &Auth{ ClientToken: resp.Auth.ClientToken, + Accessor: resp.Auth.Accessor, Policies: resp.Auth.Policies, Metadata: resp.Auth.Metadata, LeaseDuration: int(resp.Auth.TTL.Seconds()), @@ -218,6 +219,7 @@ type LogicalResponse struct { type Auth struct { ClientToken string `json:"client_token"` + Accessor string `json:"accessor"` Policies []string `json:"policies"` Metadata map[string]string `json:"metadata"` LeaseDuration int `json:"lease_duration"` diff --git a/http/logical_test.go b/http/logical_test.go index de124f7a78180..25d2472ebc499 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -140,6 +140,7 @@ func TestLogical_StandbyRedirect(t *testing.T) { testResponseBody(t, resp, &actual) actualDataMap := actual["data"].(map[string]interface{}) delete(actualDataMap, "creation_time") + delete(actualDataMap, "accessor") actual["data"] = actualDataMap delete(actual, "lease_id") if !reflect.DeepEqual(actual, expected) { @@ -180,6 +181,7 @@ func TestLogical_CreateToken(t *testing.T) { testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) delete(actual["auth"].(map[string]interface{}), "client_token") + delete(actual["auth"].(map[string]interface{}), "accessor") if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad:\nexpected:\n%#v\nactual:\n%#v", expected, actual) } diff --git a/http/sys_capabilities.go b/http/sys_capabilities.go index 48f41c778c245..93d135ed3f2dd 100644 --- a/http/sys_capabilities.go +++ b/http/sys_capabilities.go @@ -4,12 +4,11 @@ import ( "net/http" "strings" - "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/vault" ) -func handleSysCapabilities(core *vault.Core) http.Handler { +func handleSysCapabilitiesAccessor(core *vault.Core) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": @@ -19,8 +18,35 @@ func handleSysCapabilities(core *vault.Core) http.Handler { return } - // Get the auth for the request so we can access the token directly - req := requestAuth(r, &logical.Request{}) + // Parse the request if we can + var data capabilitiesAccessorRequest + if err := parseRequest(r, &data); err != nil { + respondError(w, http.StatusBadRequest, err) + return + } + + capabilities, err := core.CapabilitiesAccessor(data.Accessor, data.Path) + if err != nil { + respondErrorStatus(w, err) + return + } + + respondOk(w, &capabilitiesResponse{ + Capabilities: capabilities, + }) + }) + +} + +func handleSysCapabilities(core *vault.Core) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "PUT": + case "POST": + default: + respondError(w, http.StatusMethodNotAllowed, nil) + return + } // Parse the request if we can var data capabilitiesRequest @@ -30,18 +56,15 @@ func handleSysCapabilities(core *vault.Core) http.Handler { } if strings.HasPrefix(r.URL.Path, "/v1/sys/capabilities-self") { + // Get the auth for the request so we can access the token directly + req := requestAuth(r, &logical.Request{}) data.Token = req.ClientToken } capabilities, err := core.Capabilities(data.Token, data.Path) if err != nil { - if errwrap.ContainsType(err, new(vault.ErrUserInput)) { - respondError(w, http.StatusBadRequest, err) - return - } else { - respondError(w, http.StatusInternalServerError, err) - return - } + respondErrorStatus(w, err) + return } respondOk(w, &capabilitiesResponse{ @@ -59,3 +82,8 @@ type capabilitiesRequest struct { Token string `json:"token"` Path string `json:"path"` } + +type capabilitiesAccessorRequest struct { + Accessor string `json:"accessor"` + Path string `json:"path"` +} diff --git a/http/sys_capabilities_test.go b/http/sys_capabilities_test.go index f192e2b0a63b7..726621ec7f3fa 100644 --- a/http/sys_capabilities_test.go +++ b/http/sys_capabilities_test.go @@ -7,6 +7,78 @@ import ( "github.com/hashicorp/vault/vault" ) +func TestSysCapabilitiesAccessor(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + TestServerAuth(t, addr, token) + + // Lookup the token properties + resp := testHttpGet(t, token, addr+"/v1/auth/token/lookup/"+token) + var lookupResp map[string]interface{} + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &lookupResp) + + // Retrieve the accessor from the token properties + lookupData := lookupResp["data"].(map[string]interface{}) + accessor := lookupData["accessor"].(string) + + resp = testHttpPost(t, token, addr+"/v1/sys/capabilities-accessor", map[string]interface{}{ + "accessor": accessor, + "path": "testpath", + }) + + var actual map[string][]string + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + + expected := map[string][]string{ + "capabilities": []string{"root"}, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) + } + + // Testing for non-root token's accessor + // Create a policy first + resp = testHttpPost(t, token, addr+"/v1/sys/policy/foo", map[string]interface{}{ + "rules": `path "testpath" {capabilities = ["read","sudo"]}`, + }) + testResponseStatus(t, resp, 204) + + // Create a token against the test policy + resp = testHttpPost(t, token, addr+"/v1/auth/token/create", map[string]interface{}{ + "policies": []string{"foo"}, + }) + + var tokenResp map[string]interface{} + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &tokenResp) + + // Check if desired policies are present in the token + auth := tokenResp["auth"].(map[string]interface{}) + actualPolicies := auth["policies"] + expectedPolicies := []interface{}{"default", "foo"} + if !reflect.DeepEqual(actualPolicies, expectedPolicies) { + t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actualPolicies, expectedPolicies) + } + + // Check the capabilities of non-root token using the accessor + resp = testHttpPost(t, token, addr+"/v1/sys/capabilities-accessor", map[string]interface{}{ + "accessor": auth["accessor"], + "path": "testpath", + }) + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + + expected = map[string][]string{ + "capabilities": []string{"sudo", "read"}, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) + } +} + func TestSysCapabilities(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := TestServer(t, core) diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index 865c74857a833..fd9f42d88bac7 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -309,6 +309,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) { testResponseBody(t, resp, &actual) expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] + expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] if !reflect.DeepEqual(actual["data"], expected) { t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) @@ -389,6 +390,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { testResponseBody(t, resp, &actual) expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] + expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] if !reflect.DeepEqual(actual["data"], expected) { t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) diff --git a/logical/auth.go b/logical/auth.go index e297e10eb3ab4..1636fb5bdfe09 100644 --- a/logical/auth.go +++ b/logical/auth.go @@ -33,6 +33,13 @@ type Auth struct { // This will be filled in by Vault core when an auth structure is // returned. Setting this manually will have no effect. ClientToken string + + // Accessor is the identifier for the ClientToken. This can be used + // to perform management functionalities (especially revocation) when + // ClientToken in the audit logs are obfuscated. Accessor can be used + // to revoke a ClientToken and to lookup the capabilities of the ClientToken, + // both without actually knowing the ClientToken. + Accessor string } func (a *Auth) GoString() string { diff --git a/vault/capabilities.go b/vault/capabilities.go index fb9c1ad93486b..9c0164ed823b4 100644 --- a/vault/capabilities.go +++ b/vault/capabilities.go @@ -3,27 +3,42 @@ package vault // Struct to identify user input errors. // This is helpful in responding the appropriate status codes to clients // from the HTTP endpoints. -type ErrUserInput struct { - Message string +type StatusBadRequest struct { + Err string } // Implementing error interface -func (e *ErrUserInput) Error() string { - return e.Message +func (s *StatusBadRequest) Error() string { + return s.Err +} + +// CapabilitiesAccessor is used to fetch the capabilities of the token +// which associated with the given accessor on the given path +func (c *Core) CapabilitiesAccessor(accessor, path string) ([]string, error) { + if path == "" { + return nil, &StatusBadRequest{Err: "missing path"} + } + + if accessor == "" { + return nil, &StatusBadRequest{Err: "missing accessor"} + } + + token, err := c.tokenStore.lookupByAccessor(accessor) + if err != nil { + return nil, err + } + + return c.Capabilities(token, path) } // Capabilities is used to fetch the capabilities of the given token on the given path func (c *Core) Capabilities(token, path string) ([]string, error) { if path == "" { - return nil, &ErrUserInput{ - Message: "missing path", - } + return nil, &StatusBadRequest{Err: "missing path"} } if token == "" { - return nil, &ErrUserInput{ - Message: "missing token", - } + return nil, &StatusBadRequest{Err: "missing token"} } te, err := c.tokenStore.Lookup(token) @@ -31,9 +46,7 @@ func (c *Core) Capabilities(token, path string) ([]string, error) { return nil, err } if te == nil { - return nil, &ErrUserInput{ - Message: "invalid token", - } + return nil, &StatusBadRequest{Err: "invalid token"} } if te.Policies == nil { diff --git a/vault/capabilities_test.go b/vault/capabilities_test.go index 8367dc90bd6fb..b560787a28d40 100644 --- a/vault/capabilities_test.go +++ b/vault/capabilities_test.go @@ -5,7 +5,62 @@ import ( "testing" ) -func TestCapabilities_Basic(t *testing.T) { +func TestCapabilitiesAccessor(t *testing.T) { + c, _, token := TestCoreUnsealed(t) + + // Lookup the token in the store to get root token's accessor + tokenEntry, err := c.tokenStore.Lookup(token) + if err != nil { + t.Fatalf("err: %s", err) + } + accessor := tokenEntry.Accessor + + // Use the accessor to fetch the capabilities + actual, err := c.CapabilitiesAccessor(accessor, "path") + if err != nil { + t.Fatalf("err: %s", err) + } + expected := []string{"root"} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) + } + + // Create a policy + policy, _ := Parse(aclPolicy) + err = c.policyStore.SetPolicy(policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Create a token for the policy + ent := &TokenEntry{ + ID: "capabilitiestoken", + Path: "testpath", + Policies: []string{"dev"}, + } + if err := c.tokenStore.create(ent); err != nil { + t.Fatalf("err: %v", err) + } + + // Lookup the token in the store to get token's accessor + tokenEntry, err = c.tokenStore.Lookup("capabilitiestoken") + if err != nil { + t.Fatalf("err: %s", err) + } + accessor = tokenEntry.Accessor + + // Use the accessor to fetch the capabilities + actual, err = c.CapabilitiesAccessor(accessor, "foo/bar") + if err != nil { + t.Fatalf("err: %s", err) + } + expected = []string{"sudo", "read", "create"} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) + } +} + +func TestCapabilities(t *testing.T) { c, _, token := TestCoreUnsealed(t) actual, err := c.Capabilities(token, "path") diff --git a/vault/core_test.go b/vault/core_test.go index ec545857368e3..0d28eab0c049b 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -787,6 +787,7 @@ func TestCore_HandleLogin_Token(t *testing.T) { } expect := &TokenEntry{ ID: clientToken, + Accessor: te.Accessor, Parent: "", Policies: []string{"foo", "bar", "default"}, Path: "auth/foo/login", @@ -986,6 +987,7 @@ func TestCore_HandleRequest_CreateToken_Lease(t *testing.T) { } expect := &TokenEntry{ ID: clientToken, + Accessor: te.Accessor, Parent: root, Policies: []string{"default", "foo"}, Path: "auth/token/create", @@ -1030,6 +1032,7 @@ func TestCore_HandleRequest_CreateToken_NoDefaultPolicy(t *testing.T) { } expect := &TokenEntry{ ID: clientToken, + Accessor: te.Accessor, Parent: root, Policies: []string{"foo"}, Path: "auth/token/create", diff --git a/vault/token_store.go b/vault/token_store.go index b319e04a6eea7..c39a608c9f4dd 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -21,6 +21,10 @@ const ( // primary ID based index lookupPrefix = "id/" + // accessorPrefix is the prefix used to store the index from + // Accessor to Token ID + accessorPrefix = "accessor/" + // parentPrefix is the prefix used to store tokens for their // secondar parent based index parentPrefix = "parent/" @@ -127,6 +131,24 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) HelpDescription: strings.TrimSpace(tokenLookupHelp), }, + &framework.Path{ + Pattern: "lookup-accessor/(?P.+)", + + Fields: map[string]*framework.FieldSchema{ + "accessor": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Accessor of the token to lookup", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: t.handleUpdateLookupAccessor, + }, + + HelpSynopsis: strings.TrimSpace(lookupAccessorHelp), + HelpDescription: strings.TrimSpace(lookupAccessorHelp), + }, + &framework.Path{ Pattern: "lookup-self$", @@ -145,6 +167,24 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) HelpDescription: strings.TrimSpace(tokenLookupHelp), }, + &framework.Path{ + Pattern: "revoke-accessor/(?P.+)", + + Fields: map[string]*framework.FieldSchema{ + "accessor": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Accessor of the token", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: t.handleUpdateRevokeAccessor, + }, + + HelpSynopsis: strings.TrimSpace(revokeAccessorHelp), + HelpDescription: strings.TrimSpace(revokeAccessorHelp), + }, + &framework.Path{ Pattern: "revoke-self$", @@ -264,6 +304,7 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) // TokenEntry is used to represent a given token type TokenEntry struct { ID string // ID of this entry, generally a random UUID + Accessor string // Accessor for this token, a random UUID Parent string // Parent token, used for revocation trees Policies []string // Which named policies should be used Path string // Used for audit trails, this is something like "auth/user/login" @@ -300,6 +341,27 @@ func (ts *TokenStore) rootToken() (*TokenEntry, error) { return te, nil } +// createAccessor is used to create an identifier for the token ID. +// A storage index, mapping the accessor to the token ID is also created. +func (ts *TokenStore) createAccessor(entry *TokenEntry) error { + defer metrics.MeasureSince([]string{"token", "createAccessor"}, time.Now()) + + // Create a random accessor + accessorUUID, err := uuid.GenerateUUID() + if err != nil { + return err + } + entry.Accessor = accessorUUID + + // Create index entry, mapping the accessor to the token ID + path := accessorPrefix + ts.SaltID(entry.Accessor) + le := &logical.StorageEntry{Key: path, Value: []byte(entry.ID)} + if err := ts.view.Put(le); err != nil { + return fmt.Errorf("failed to persist accessor index entry: %v", err) + } + return nil +} + // Create is used to create a new token entry. The entry is assigned // a newly generated ID if not provided. func (ts *TokenStore) create(entry *TokenEntry) error { @@ -313,6 +375,11 @@ func (ts *TokenStore) create(entry *TokenEntry) error { entry.ID = entryUUID } + err := ts.createAccessor(entry) + if err != nil { + return err + } + return ts.storeCommon(entry, true) } @@ -469,6 +536,14 @@ func (ts *TokenStore) revokeSalted(saltedId string) error { } } + // Clear the accessor index if any + if entry != nil && entry.Accessor != "" { + path := accessorPrefix + ts.SaltID(entry.Accessor) + if ts.view.Delete(path); err != nil { + return fmt.Errorf("failed to delete entry: %v", err) + } + } + // Revoke all secrets under this token if entry != nil { if err := ts.expiration.RevokeByToken(entry.ID); err != nil { @@ -530,6 +605,83 @@ func (ts *TokenStore) revokeTreeSalted(saltedId string) error { return nil } +func (ts *TokenStore) lookupByAccessor(accessor string) (string, error) { + entry, err := ts.view.Get(accessorPrefix + ts.SaltID(accessor)) + if err != nil { + return "", fmt.Errorf("failed to read index using accessor: %s", err) + } + if entry == nil { + return "", &StatusBadRequest{Err: "invalid accessor"} + } + + return string(entry.Value), nil +} + +// handleUpdateLookupAccessor handles the auth/token/lookup-accessor path for returning +// the properties of the token associated with the accessor +func (ts *TokenStore) handleUpdateLookupAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + accessor := data.Get("accessor").(string) + if accessor == "" { + return nil, &StatusBadRequest{Err: "missing accessor"} + } + + tokenID, err := ts.lookupByAccessor(accessor) + if err != nil { + return nil, err + } + + // Prepare the field data required for a lookup call + d := &framework.FieldData{ + Raw: map[string]interface{}{ + "token": tokenID, + }, + Schema: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Token to lookup", + }, + }, + } + resp, err := ts.handleLookup(req, d) + if err != nil { + return nil, err + } + if resp == nil { + return nil, fmt.Errorf("failed to lookup the token") + } + if resp.IsError() { + return resp, nil + + } + + // Remove the token ID from the response + if resp.Data != nil { + resp.Data["id"] = "" + } + + return resp, nil +} + +// handleUpdateRevokeAccessor handles the auth/token/revoke-accessor path for revoking +// the token associated with the accessor +func (ts *TokenStore) handleUpdateRevokeAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + accessor := data.Get("accessor").(string) + if accessor == "" { + return nil, &StatusBadRequest{Err: "missing accessor"} + } + + tokenID, err := ts.lookupByAccessor(accessor) + if err != nil { + return nil, err + } + + // Revoke the token and its children + if err := ts.RevokeTree(tokenID); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + return nil, nil +} + // handleCreate handles the auth/token/create path for creation of new orphan // tokens func (ts *TokenStore) handleCreateOrphan( @@ -705,6 +857,7 @@ func (ts *TokenStore) handleCreateCommon( Renewable: true, }, ClientToken: te.ID, + Accessor: te.Accessor, }, } @@ -831,6 +984,7 @@ func (ts *TokenStore) handleLookup( resp := &logical.Response{ Data: map[string]interface{}{ "id": out.ID, + "accessor": out.Accessor, "policies": out.Policies, "path": out.Path, "meta": out.Meta, @@ -921,8 +1075,10 @@ as revocation of tokens. The tokens are renewable if associated with a lease.` tokenCreateHelp = `The token create path is used to create new tokens.` tokenCreateOrphanHelp = `The token create path is used to create new orphan tokens.` tokenLookupHelp = `This endpoint will lookup a token and its properties.` + lookupAccessorHelp = `This endpoint will lookup a token associated with the given accessor and its properties. Response will not contain the token ID.` tokenRevokeHelp = `This endpoint will delete the given token and all of its child tokens.` tokenRevokeSelfHelp = `This endpoint will delete the token used to call it and all of its child tokens.` + revokeAccessorHelp = `This endpoint will delete the token associated with the accessor and all of its child tokens.` tokenRevokeOrphanHelp = `This endpoint will delete the token and orphan its child tokens.` tokenRevokePrefixHelp = `This endpoint will delete all tokens generated under a prefix with their child tokens.` tokenRenewHelp = `This endpoint will renew the given token and prevent expiration.` diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 66b7f06e97f6d..00555de34a636 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -18,6 +18,94 @@ func getBackendConfig(c *Core) *logical.BackendConfig { } } +func TestTokenStore_AccessorIndex(t *testing.T) { + _, ts, _, _ := TestCoreWithTokenStore(t) + + ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}} + if err := ts.create(ent); err != nil { + t.Fatalf("err: %s", err) + } + + out, err := ts.Lookup(ent.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure that accessor is created + if out == nil || out.Accessor == "" { + t.Fatalf("bad: %#v", out) + } + + token, err := ts.lookupByAccessor(out.Accessor) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Verify that the value returned from the index matches the token ID + if token != ent.ID { + t.Fatalf("bad: got\n%s\nexpected\n%s\n", token, ent.ID) + } +} + +func TestTokenStore_HandleRequest_LookupAccessor(t *testing.T) { + _, ts, _, root := TestCoreWithTokenStore(t) + testMakeToken(t, ts, root, "tokenid", "", []string{"foo"}) + out, err := ts.Lookup("tokenid") + if err != nil { + t.Fatalf("err: %s", err) + } + if out == nil { + t.Fatalf("err: %s", err) + } + + req := logical.TestRequest(t, logical.UpdateOperation, "lookup-accessor/"+out.Accessor) + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %s", err) + } + if resp.Data == nil { + t.Fatalf("response should contain data") + } + + if resp.Data["accessor"].(string) == "" { + t.Fatalf("accessor should not be empty") + } + + // Verify that the lookup-accessor operation does not return the token ID + if resp.Data["id"].(string) != "" { + t.Fatalf("token ID should not be returned") + } +} + +func TestTokenStore_HandleRequest_RevokeAccessor(t *testing.T) { + _, ts, _, root := TestCoreWithTokenStore(t) + testMakeToken(t, ts, root, "tokenid", "", []string{"foo"}) + out, err := ts.Lookup("tokenid") + if err != nil { + t.Fatalf("err: %s", err) + } + if out == nil { + t.Fatalf("err: %s", err) + } + + req := logical.TestRequest(t, logical.UpdateOperation, "revoke-accessor/"+out.Accessor) + + _, err = ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %s", err) + } + + out, err = ts.Lookup("tokenid") + if err != nil { + t.Fatalf("err: %s", err) + } + + if out != nil { + t.Fatalf("bad:\ngot %#v\nexpected: nil\n", out) + } +} + func TestTokenStore_RootToken(t *testing.T) { _, ts, _, _ := TestCoreWithTokenStore(t) @@ -382,6 +470,7 @@ func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) { expected := &TokenEntry{ ID: resp.Auth.ClientToken, + Accessor: resp.Auth.Accessor, Parent: root, Policies: []string{"root"}, Path: "auth/token/create", @@ -412,6 +501,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) { expected := &TokenEntry{ ID: resp.Auth.ClientToken, + Accessor: resp.Auth.Accessor, Parent: root, Policies: []string{"root"}, Path: "auth/token/create", @@ -475,6 +565,7 @@ func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) { expected := &TokenEntry{ ID: resp.Auth.ClientToken, + Accessor: resp.Auth.Accessor, Parent: root, Policies: []string{"root"}, Path: "auth/token/create", @@ -831,6 +922,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { exp := map[string]interface{}{ "id": root, + "accessor": resp.Data["accessor"].(string), "policies": []string{"root"}, "path": "auth/token/root", "meta": map[string]string(nil), @@ -863,6 +955,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { exp = map[string]interface{}{ "id": "client", + "accessor": resp.Data["accessor"], "policies": []string{"default", "foo"}, "path": "auth/token/create", "meta": map[string]string(nil), @@ -965,6 +1058,7 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { exp := map[string]interface{}{ "id": root, + "accessor": resp.Data["accessor"], "policies": []string{"root"}, "path": "auth/token/root", "meta": map[string]string(nil), diff --git a/website/source/docs/auth/token.html.md b/website/source/docs/auth/token.html.md index 0f7df70ab97dc..1a1a0d0a23a7f 100644 --- a/website/source/docs/auth/token.html.md +++ b/website/source/docs/auth/token.html.md @@ -412,3 +412,85 @@ of the header should be "X-Vault-Token" and the value should be the token. +### /auth/token/lookup-accessor +#### POST + +
+
Description
+
+ Fetch the properties of the token associated with the accessor, except the token ID. + This is meant for purposes where there is no access to token ID but there is need + to fetch the properties of a token. +
+ +
Method
+
POST
+ +
URL
+
`/auth/token/lookup-accessor`
+ +
Parameters
+
+
    +
  • + accessor + required + Accessor of the token to lookup. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "creation_time": 1457533232, + "creation_ttl": 2592000, + "display_name": "token", + "id": "", + "meta": null, + "num_uses": 0, + "orphan": false, + "path": "auth/token/create", + "policies": ["default", "web"], + "ttl": 2591976 + }, + "warnings": null, + "auth": null + } + ``` +
+
+ +### /auth/token/revoke-accessor/ +#### POST + +
+
Description
+
+ Revoke the token associated with the accessor and all the child tokens. + This is meant for purposes where there is no access to token ID but + there is need to revoke a token and its children. +
+ +
Method
+
POST
+ +
URL
+
`/auth/token/revoke-accessor/`
+ +
Parameters
+
+ None +
+ +
Returns
+
`204` response code. +
+
+ diff --git a/website/source/docs/http/sys-capabilities-accessor.html.md b/website/source/docs/http/sys-capabilities-accessor.html.md new file mode 100644 index 0000000000000..4549b203fbb72 --- /dev/null +++ b/website/source/docs/http/sys-capabilities-accessor.html.md @@ -0,0 +1,48 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/capabilities-accessor" +sidebar_current: "docs-http-auth-capabilities-accessor" +description: |- + The `/sys/capabilities-accessor` endpoint is used to fetch the capabilities of the token associated with an accessor, on the given path. +--- + +# /sys/capabilities-accessor + +## POST + +
+
Description
+
+ Returns the capabilities of the token associated with an accessor, on the given path. +
+ +
Method
+
POST
+ +
Parameters
+
+
    +
  • + accessor + required + Accessor of the token. +
  • +
  • + path + required + Path on which the token's capabilities will be checked. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "capabilities": ["read", "list"] + } + ``` + +
+
diff --git a/website/source/layouts/http.erb b/website/source/layouts/http.erb index b8ea3443e11f7..5b61c1262ac42 100644 --- a/website/source/layouts/http.erb +++ b/website/source/layouts/http.erb @@ -77,6 +77,10 @@ > /sys/capabilities-self + + > + /sys/capabilities-accessor +