Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessor paths for lookup and revocation of tokens #1188

Merged
merged 20 commits into from Mar 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
38a5d75
Introduced AccessorID in TokenEntry and returning it along with token
vishalnayak Mar 8, 2016
a713720
Create indexing from Accessor ID to Token ID
vishalnayak Mar 8, 2016
4ed3a85
Clear the accessor index during revocation
vishalnayak Mar 8, 2016
c7033b1
placeholders for revoke-accessor and lookup-accessor
vishalnayak Mar 8, 2016
bb927e3
Implemented lookup-accessor as a token_store endpoint
vishalnayak Mar 8, 2016
5dcc6f0
Implemented /auth/token/revoke-accessor in token_store
vishalnayak Mar 8, 2016
048f3b2
Lay the foundation for returning proper HTTP status codes
vishalnayak Mar 8, 2016
9da2929
Implemented /sys/capabilities-accessor and a way for setting HTTP err…
vishalnayak Mar 9, 2016
edfba16
ErrUserInput --> StatusBadRequest
vishalnayak Mar 9, 2016
7b99652
Error text corrections and minor refactoring
vishalnayak Mar 9, 2016
2a35de8
AccessorID --> Accessor, accessor_id --> accessor
vishalnayak Mar 9, 2016
c7c9e0b
New prefix for accessor indexes
vishalnayak Mar 9, 2016
928d872
Add docs for new token endpoints
vishalnayak Mar 9, 2016
16c4b52
Added docs for /sys/capabilities-accessor
vishalnayak Mar 9, 2016
a546823
Added tests for 'sys/capabilities-accessor' endpoint
vishalnayak Mar 9, 2016
76900d6
Added tests for lookup-accessor and revoke-accessor endpoints
vishalnayak Mar 9, 2016
da9ad9c
Provide accessor to revove-accessor in the URL itself
vishalnayak Mar 9, 2016
d1d37d5
fix all the broken tests
vishalnayak Mar 9, 2016
64bc542
Restore old regex expressions for token endpoints
vishalnayak Mar 9, 2016
b8bd534
In-URL accessor for auth/token/lookup-accessor endpoint
vishalnayak Mar 9, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions api/secret.go
Expand Up @@ -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"`

Expand Down
1 change: 1 addition & 0 deletions command/format.go
Expand Up @@ -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))
Expand Down
16 changes: 15 additions & 1 deletion http/handler.go
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"
"strings"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
Expand All @@ -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))

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you have multiple types of errors generated? Which one trumps the other?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the latest error type will be considered. In order for this to work as expected, we should follow a practice, i.e. when err != nil, we should always do return err instead of return fmt.Errorf("err:%s",err). This way, the error type once set, will retain it's type all the way back into this method.

Since we were only generating internal error till date, I guess it should be fine if we start this practice now. This might not affect any of the existing use-cases.

The status code is not being set anywhere after any call crosses core, except logical.HTTPStatusCode which is a different code flow.

In case of multiple errors using multierror, the default case will be StatusInternalServerError. It makes sense as well. There will be more than one error and we'll not know which status to set (unless there are predefined priorities to status codes, are there?).

Also, since multierror implements error interface, user will get to see the all the errors in a formatted manner, and hopefully not complain for the proper status code.

// 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 {
Expand Down
2 changes: 2 additions & 0 deletions http/logical.go
Expand Up @@ -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()),
Expand Down Expand Up @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions http/logical_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
50 changes: 39 additions & 11 deletions http/sys_capabilities.go
Expand Up @@ -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":
Expand All @@ -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
Expand All @@ -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{
Expand All @@ -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"`
}
72 changes: 72 additions & 0 deletions http/sys_capabilities_test.go
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions http/sys_generate_root_test.go
Expand Up @@ -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"])
Expand Down Expand Up @@ -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"])
Expand Down
7 changes: 7 additions & 0 deletions logical/auth.go
Expand Up @@ -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 {
Expand Down
39 changes: 26 additions & 13 deletions vault/capabilities.go
Expand Up @@ -3,37 +3,50 @@ 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)
if err != nil {
return nil, err
}
if te == nil {
return nil, &ErrUserInput{
Message: "invalid token",
}
return nil, &StatusBadRequest{Err: "invalid token"}
}

if te.Policies == nil {
Expand Down