diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 9ae799cd1..b54db12cb 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -30,7 +30,7 @@ func mockExpiration(t *testing.T) *ExpirationManager { // Create the barrier view view := NewBarrierView(b, "expire/") - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) router := NewRouter() logger := log.New(os.Stderr, "", log.LstdFlags) diff --git a/vault/token_store.go b/vault/token_store.go index 63eed8f5e..cce949ae9 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "strings" + "time" "github.com/hashicorp/vault/credential" "github.com/hashicorp/vault/logical" @@ -294,28 +296,155 @@ func (ts *TokenStore) RevokeAll() error { // HandleRequest is used to handle a request and generate a response. // The backends must check the operation type and handle appropriately. -func (ts *TokenStore) HandleRequest(*logical.Request) (*logical.Response, error) { - return nil, logical.ErrUnsupportedOperation +func (ts *TokenStore) HandleRequest(req *logical.Request) (*logical.Response, error) { + switch { + case req.Path == "create": + return ts.handleCreate(req) + case strings.HasPrefix(req.Path, "revoke-orphan/"): + return ts.handleRevokeOrphan(req) + case req.Path == "": + switch req.Operation { + case logical.HelpOperation: + return logical.HelpResponse(tokenBackendHelp, []string{"auth/token/create"}), nil + default: + return nil, logical.ErrUnsupportedOperation + } + } + return nil, logical.ErrUnsupportedPath } -// RootPaths is a list of paths that require root level privileges. -// These paths will be enforced by the router so that backends do -// not need to handle the authorization. Paths are enforced exactly -// or using a prefix match if they end in '*' func (ts *TokenStore) RootPaths() []string { return nil } -// LoginPaths is a list of paths that are unauthenticated and used -// only for logging in. These paths cannot be reached via HandleRequest, -// and are sent to HandleLogin instead. Paths are enforced exactly -// or using a prefix match if they end in '*' func (ts *TokenStore) LoginPaths() []string { return nil } -// HandleLogin is used to handle a login request and generate a response. -// The backend is allowed to ignore this request if it is not applicable. func (ts *TokenStore) HandleLogin(req *credential.Request) (*credential.Response, error) { + return nil, logical.ErrUnsupportedOperation +} + +// handleCreate handles the auth/token/create path for creation of new tokens +func (ts *TokenStore) handleCreate(req *logical.Request) (*logical.Response, error) { + // Validate the operation + switch req.Operation { + case logical.WriteOperation: + case logical.HelpOperation: + return logical.HelpResponse(tokenCreateHelp, nil), nil + default: + return nil, logical.ErrUnsupportedOperation + } + + // Read the parent policy + parent, err := ts.Lookup(req.ClientToken) + if err != nil || parent == nil { + return logical.ErrorResponse("parent token lookup failed"), logical.ErrInvalidRequest + } + + // Check if the parent policy is root + isRoot := strListContains(parent.Policies, "root") + + // Read and parse the fields + idRaw, _ := req.Data["id"] + policiesRaw, _ := req.Data["policies"] + metaRaw, _ := req.Data["meta"] + noParentRaw, _ := req.Data["no_parent"] + leaseRaw, _ := req.Data["lease"] + + // Setup the token entry + te := TokenEntry{ + Parent: req.ClientToken, + Path: "auth/token/create", + } + + // Allow specifying the ID of the token if the client is root + if id, ok := idRaw.(string); ok { + if !isRoot { + return logical.ErrorResponse("root required to specify token id"), + logical.ErrInvalidRequest + } + te.ID = id + } + + // Only permit policies to be a subset unless the client is root + if policies, ok := policiesRaw.([]string); ok { + if !isRoot && !strListSubset(parent.Policies, policies) { + return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest + } + te.Policies = policies + } + + // Ensure is some associated policy + if len(te.Policies) == 0 { + return logical.ErrorResponse("token must have at least one policy"), logical.ErrInvalidRequest + } + + // Only allow an orphan token if the client is root + if noParent, _ := noParentRaw.(bool); noParent { + if !isRoot { + return logical.ErrorResponse("root required to create orphan token"), + logical.ErrInvalidRequest + } + te.Parent = "" + } + + // Parse any metadata associated with the token + if meta, ok := metaRaw.(map[string]interface{}); ok { + te.Meta = meta + } + + // Parse the lease if any + var secret *logical.Secret + if lease, ok := leaseRaw.(string); ok { + dur, err := time.ParseDuration(lease) + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + if dur < 0 { + return logical.ErrorResponse("lease must be positive"), logical.ErrInvalidRequest + } + secret = &logical.Secret{ + Lease: dur, + LeaseGracePeriod: dur / 10, // Provide a 10% grace buffer + Renewable: true, + } + } + + // Create the token + if err := ts.Create(&te); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + + // Generate the response + resp := &logical.Response{ + Secret: secret, + Data: map[string]interface{}{ + clientTokenKey: te.ID, + }, + } + return resp, nil +} + +// handleRevokeOrphan handles the auth/token/revoke-orphan/id path for revocation of tokens +// in a way that leaves child tokens orphaned. Normally, using sys/revoke/vaultID will revoke +// the token and all children. +func (ts *TokenStore) handleRevokeOrphan(req *logical.Request) (*logical.Response, error) { + // Validate the operation + switch req.Operation { + case logical.WriteOperation: + default: + return nil, logical.ErrUnsupportedOperation + } + return nil, nil } + +const ( + tokenBackendHelp = `The token credential backend is always enabled and builtin to Vault. +Client tokens are used to identify a client and to allow Vault to associate policies and ACLs +which are enforced on every request. This backend also allows for generating sub-tokens as well +as revocation of tokens.` + + tokenCreateHelp = `The token create path is used to create new tokens.` +) diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 3a78a4eab..c71ff2d66 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -3,19 +3,22 @@ package vault import ( "reflect" "testing" + "time" + + "github.com/hashicorp/vault/logical" ) -func mockTokenStore(t *testing.T) (*Core, *TokenStore) { - c, _ := TestCoreUnsealed(t) +func mockTokenStore(t *testing.T) (*Core, *TokenStore, string) { + c, _, root := TestCoreUnsealedToken(t) ts, err := NewTokenStore(c) if err != nil { t.Fatalf("err: %v", err) } - return c, ts + return c, ts, root } func TestTokenStore_RootToken(t *testing.T) { - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) te, err := ts.RootToken() if err != nil { @@ -35,7 +38,7 @@ func TestTokenStore_RootToken(t *testing.T) { } func TestTokenStore_CreateLookup(t *testing.T) { - c, ts := mockTokenStore(t) + c, ts, _ := mockTokenStore(t) ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}} if err := ts.Create(ent); err != nil { @@ -70,7 +73,7 @@ func TestTokenStore_CreateLookup(t *testing.T) { } func TestTokenStore_CreateLookup_ProvidedID(t *testing.T) { - c, ts := mockTokenStore(t) + c, ts, _ := mockTokenStore(t) ent := &TokenEntry{ ID: "foobarbaz", @@ -109,7 +112,7 @@ func TestTokenStore_CreateLookup_ProvidedID(t *testing.T) { } func TestTokenStore_Revoke(t *testing.T) { - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}} if err := ts.Create(ent); err != nil { @@ -135,7 +138,7 @@ func TestTokenStore_Revoke(t *testing.T) { } func TestTokenStore_Revoke_Orphan(t *testing.T) { - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}} if err := ts.Create(ent); err != nil { @@ -162,7 +165,7 @@ func TestTokenStore_Revoke_Orphan(t *testing.T) { } func TestTokenStore_RevokeTree(t *testing.T) { - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) ent1 := &TokenEntry{} if err := ts.Create(ent1); err != nil { @@ -206,7 +209,7 @@ func TestTokenStore_RevokeTree(t *testing.T) { } func TestTokenStore_RevokeAll(t *testing.T) { - _, ts := mockTokenStore(t) + _, ts, _ := mockTokenStore(t) ent1 := &TokenEntry{} if err := ts.Create(ent1); err != nil { @@ -244,3 +247,222 @@ func TestTokenStore_RevokeAll(t *testing.T) { } } } + +func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + + resp, err := ts.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data["error"] != "token must have at least one policy" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_BadParent(t *testing.T) { + _, ts, _ := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = "random" + + resp, err := ts.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data["error"] != "parent token lookup failed" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] == "" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_RootID(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["id"] = "foobar" + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] != "foobar" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NonRootID(t *testing.T) { + _, ts, root := mockTokenStore(t) + testMakeToken(t, ts, root, "client", []string{"foo"}) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = "client" + req.Data["id"] = "foobar" + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data["error"] != "root required to specify token id" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NonRoot_Subset(t *testing.T) { + _, ts, root := mockTokenStore(t) + testMakeToken(t, ts, root, "client", []string{"foo", "bar"}) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = "client" + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] == "" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NonRoot_InvalidSubset(t *testing.T) { + _, ts, root := mockTokenStore(t) + testMakeToken(t, ts, root, "client", []string{"foo", "bar"}) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = "client" + req.Data["policies"] = []string{"foo", "bar", "baz"} + + resp, err := ts.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data["error"] == "child policies must be subset of parent" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NonRoot_NoParent(t *testing.T) { + _, ts, root := mockTokenStore(t) + testMakeToken(t, ts, root, "client", []string{"foo"}) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = "client" + req.Data["no_parent"] = true + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data["error"] != "root required to create orphan token" { + t.Fatalf("bad: %#v", resp) + } +} + +func TestTokenStore_HandleRequest_CreateToken_Root_NoParent(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["no_parent"] = true + req.Data["policies"] = []string{"foo"} + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] == "" { + t.Fatalf("bad: %#v", resp) + } + + out, _ := ts.Lookup(resp.Data[clientTokenKey].(string)) + if out.Parent != "" { + t.Fatalf("bad: %#v", out) + } +} + +func TestTokenStore_HandleRequest_CreateToken_Metadata(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["policies"] = []string{"foo"} + meta := map[string]interface{}{ + "user": "armon", + "source": "github", + } + req.Data["meta"] = meta + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] == "" { + t.Fatalf("bad: %#v", resp) + } + + out, _ := ts.Lookup(resp.Data[clientTokenKey].(string)) + if !reflect.DeepEqual(out.Meta, meta) { + t.Fatalf("bad: %#v", out) + } +} + +func TestTokenStore_HandleRequest_CreateToken_Lease(t *testing.T) { + _, ts, root := mockTokenStore(t) + + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["policies"] = []string{"foo"} + req.Data["lease"] = "1h" + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] == "" { + t.Fatalf("bad: %#v", resp) + } + if resp.Secret.Lease != time.Hour { + t.Fatalf("bad: %#v", resp) + } + if !resp.Secret.Renewable { + t.Fatalf("bad: %#v", resp) + } +} + +func testMakeToken(t *testing.T, ts *TokenStore, root, client string, policy []string) { + req := logical.TestRequest(t, logical.WriteOperation, "create") + req.ClientToken = root + req.Data["id"] = client + req.Data["policies"] = policy + + resp, err := ts.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp.Data[clientTokenKey] != "client" { + t.Fatalf("bad: %#v", resp) + } +}