Skip to content

Commit

Permalink
Merge pull request #1188 from hashicorp/token-accessors
Browse files Browse the repository at this point in the history
Accessor paths for lookup and revocation of tokens
  • Loading branch information
vishalnayak committed Mar 9, 2016
2 parents 23b9397 + b8bd534 commit c2d8536
Show file tree
Hide file tree
Showing 17 changed files with 610 additions and 26 deletions.
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
// 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

0 comments on commit c2d8536

Please sign in to comment.