From 1ec78727b52771642b98047f9cf4c67b4d4acbcc Mon Sep 17 00:00:00 2001 From: Michel Vocks Date: Fri, 5 Apr 2019 15:36:29 +0200 Subject: [PATCH] Implemented token backend support for identity --- api/auth_token.go | 1 + command/token_create.go | 12 ++ vault/token_store.go | 93 +++++++++++++-- vault/token_store_test.go | 243 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 9 deletions(-) diff --git a/api/auth_token.go b/api/auth_token.go index ed594eee8528..6807c89c3878 100644 --- a/api/auth_token.go +++ b/api/auth_token.go @@ -272,4 +272,5 @@ type TokenCreateRequest struct { NumUses int `json:"num_uses"` Renewable *bool `json:"renewable,omitempty"` Type string `json:"type"` + EntityAlias string `json:"entity_alias"` } diff --git a/command/token_create.go b/command/token_create.go index 7c454ff9ba9c..e564a3375a8f 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -29,6 +29,7 @@ type TokenCreateCommand struct { flagType string flagMetadata map[string]string flagPolicies []string + flagEntityAlias string } func (c *TokenCreateCommand) Synopsis() string { @@ -176,6 +177,16 @@ func (c *TokenCreateCommand) Flags() *FlagSets { "specified multiple times to attach multiple policies.", }) + f.StringVar(&StringVar{ + Name: "entity-alias", + Target: &c.flagEntityAlias, + Default: "", + Usage: "Name of the entity alias to associate with during token creation. " + + "Only works in combination with -role argument and used entity alias " + + "must be listed in allowed entity aliases. If this has been specified, " + + "the entity will not be inherited from the parent.", + }) + return set } @@ -224,6 +235,7 @@ func (c *TokenCreateCommand) Run(args []string) int { ExplicitMaxTTL: c.flagExplicitMaxTTL.String(), Period: c.flagPeriod.String(), Type: c.flagType, + EntityAlias: c.flagEntityAlias, } var secret *api.Secret diff --git a/vault/token_store.go b/vault/token_store.go index 31789ca878f2..1e783d72d298 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -14,10 +14,10 @@ import ( "strings" "time" - proto "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/proto" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" - sockaddr "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/go-sockaddr" metrics "github.com/armon/go-metrics" multierror "github.com/hashicorp/go-multierror" @@ -184,6 +184,11 @@ func (ts *TokenStore) paths() []*framework.Path { Default: "service", Description: "The type of token to generate, service or batch", }, + + "allowed_entity_aliases": &framework.FieldSchema{ + Type: framework.TypeStringSlice, + Description: "String or JSON list of allowed entity aliases. If set, specifies the entity aliases which are allowed to be used during token generation.", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -614,6 +619,9 @@ type tsRoleEntry struct { // The type of token this role should issue TokenType logical.TokenType `json:"token_type" mapstructure:"token_type"` + + // The set of allowed entity aliases used during token creation + AllowedEntityAliases []string `json:"allowed_entity_aliases" mapstructure:"allowed_entity_aliases" structs:"allowed_entity_aliases"` } type accessorEntry struct { @@ -1822,11 +1830,11 @@ func (ts *TokenStore) handleTidy(ctx context.Context, req *logical.Request, data } var countAccessorList, - countCubbyholeKeys, - deletedCountAccessorEmptyToken, - deletedCountAccessorInvalidToken, - deletedCountInvalidTokenInAccessor, - deletedCountInvalidCubbyholeKey int64 + countCubbyholeKeys, + deletedCountAccessorEmptyToken, + deletedCountAccessorInvalidToken, + deletedCountInvalidTokenInAccessor, + deletedCountInvalidCubbyholeKey int64 validCubbyholeKeys := make(map[string]bool) @@ -2109,6 +2117,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque NumUses int `mapstructure:"num_uses"` Period string Type string `mapstructure:"type"` + EntityAlias string } if err := mapstructure.WeakDecode(req.Data, &data); err != nil { return logical.ErrorResponse(fmt.Sprintf( @@ -2205,6 +2214,60 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque logical.ErrInvalidRequest } + // Verify the entity alias + overwriteEntityID := "" + if data.EntityAlias != "" { + // Parameter is only allowed in combination with token role + if role == nil { + return logical.ErrorResponse("'entity_alias' is only allowed in combination with token role"), logical.ErrInvalidRequest + } + + // Get mount accessor which is required to lookup entity alias + mountValidationResp := ts.core.router.validateMountByAccessor("auth_token_") + if mountValidationResp == nil { + return logical.ErrorResponse("auth token mount accessor not found"), nil + } + + // Verify that the alias exist + aliasByFactors, err := ts.core.identityStore.MemDBAliasByFactors(mountValidationResp.MountAccessor, data.EntityAlias, false, false) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + switch { + case aliasByFactors == nil: + // Entity alias does not exist. Create a new entity and entity alias + newAlias := &logical.Alias{ + Name: data.EntityAlias, + MountAccessor: mountValidationResp.MountAccessor, + MountType: mountValidationResp.MountType, + } + + newEntity, err := ts.core.identityStore.CreateOrFetchEntity(ctx, newAlias) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + // Set new entity id + overwriteEntityID = newEntity.ID + default: + // Verify that the specified entity alias is included in the allowed entity alias list + foundEntityAlias := false + for _, entityAlias := range role.AllowedEntityAliases { + if strings.Compare(entityAlias, data.EntityAlias) == 0 { + foundEntityAlias = true + } + } + + if !foundEntityAlias { + return logical.ErrorResponse("invalid 'entity_alias' value"), logical.ErrInvalidRequest + } + + // Set new entity id + overwriteEntityID = aliasByFactors.CanonicalID + } + } + // Setup the token entry te := logical.TokenEntry{ Parent: req.ClientToken, @@ -2427,9 +2490,14 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque } // At this point, it is clear whether the token is going to be an orphan or - // not. If the token is not going to be an orphan, inherit the parent's + // not. If setEntityID is set, the entity identifier will be overwritten. + // Otherwise, if the token is not going to be an orphan, inherit the parent's // entity identifier into the child token. - if te.Parent != "" { + switch { + case overwriteEntityID != "": + // Overwrite the entity identifier + te.EntityID = overwriteEntityID + case te.Parent != "": te.EntityID = parent.EntityID // If the parent has bound CIDRs, copy those into the child. We don't @@ -3135,6 +3203,13 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(ctx context.Context, req *logic } } + allowedEntityAliasesRaw, ok := data.GetOk("allowed_entity_aliases") + if ok { + entry.AllowedEntityAliases = strutil.RemoveDuplicates(allowedEntityAliasesRaw.([]string), true) + } else if req.Operation == logical.CreateOperation { + entry.AllowedEntityAliases = strutil.RemoveDuplicates(data.Get("allowed_entity_aliases").([]string), true) + } + ns, err := namespace.FromContext(ctx) if err != nil { return nil, err diff --git a/vault/token_store_test.go b/vault/token_store_test.go index bbfd4566a56c..6476254c0631 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -2612,6 +2612,249 @@ func TestTokenStore_HandleRequest_Renew(t *testing.T) { } } +func TestTokenStore_HandleRequest_CreateToken_ExistingEntityAlias(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + i := core.identityStore + ctx := namespace.RootContext(nil) + testPolicyName := "testpolicy" + entityAliasName := "testentityalias" + testRoleName := "test" + + // Create manually an entity + resp, err := i.HandleRequest(ctx, &logical.Request{ + Path: "entity", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "name": "testentity", + "policies": []string{testPolicyName}, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + entityID := resp.Data["id"].(string) + + // Find mount accessor + resp, err = core.systemBackend.HandleRequest(namespace.RootContext(nil), &logical.Request{ + Path: "auth", + Operation: logical.ReadOperation, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) + } + tokenMountAccessor := resp.Data["token/"].(map[string]interface{})["accessor"].(string) + + // Create manually an entity alias + resp, err = i.HandleRequest(ctx, &logical.Request{ + Path: "entity-alias", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "name": entityAliasName, + "canonical_id": entityID, + "mount_accessor": tokenMountAccessor, + }, + }) + + // Create token role + resp, err = core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/roles/" + testRoleName, + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "orphan": true, + "period": "72h", + "path_suffix": "happenin", + "bound_cidrs": []string{"0.0.0.0/0"}, + "allowed_entity_aliases": []string{"test1", "test2", entityAliasName}, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + + resp, err = core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/create", + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "role_name": testRoleName, + "entity_alias": entityAliasName, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) + } + if resp == nil { + t.Fatal("expected a response") + } + if resp.Auth.EntityID != entityID { + t.Fatalf("expected '%s' got '%s'", entityID, resp.Auth.EntityID) + } + + policyFound := false + for _, policy := range resp.Auth.IdentityPolicies { + if policy == testPolicyName { + policyFound = true + } + } + if !policyFound { + t.Fatalf("Policy '%s' not derived by entity but should be. Auth %#v", testPolicyName, resp.Auth) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NonExistingEntityAlias(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + i := core.identityStore + ctx := namespace.RootContext(nil) + entityAliasName := "testentityalias" + testRoleName := "test" + + // Create token role + resp, err := core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/roles/" + testRoleName, + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "period": "72h", + "path_suffix": "happenin", + "bound_cidrs": []string{"0.0.0.0/0"}, + "allowed_entity_aliases": []string{"test1", "test2"}, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + + // Create token with non existing entity alias + resp, err = core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/create", + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "role_name": testRoleName, + "entity_alias": entityAliasName, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) + } + if resp == nil { + t.Fatal("expected a response") + } + + // Read the new entity + resp, err = i.HandleRequest(ctx, &logical.Request{ + Path: "entity/id/" + resp.Auth.EntityID, + Operation: logical.ReadOperation, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) + } + + // Get the attached alias information + aliases, ok := resp.Data["aliases"].([]identity.Alias) + if !ok { + t.Fatalf("failed to parse attached aliases. Resp: %#v", resp.Data) + } + if len(aliases) != 1 { + t.Fatalf("expected one attached alias but got %d", len(aliases)) + } + if aliases[0].Name != entityAliasName { + t.Fatalf("alias name should be '%s' but is '%s'", entityAliasName, aliases[0].Name) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NotAllowedEntityAlias(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + i := core.identityStore + ctx := namespace.RootContext(nil) + testPolicyName := "testpolicy" + entityAliasName := "testentityalias" + testRoleName := "test" + + // Create manually an entity + resp, err := i.HandleRequest(ctx, &logical.Request{ + Path: "entity", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "name": "testentity", + "policies": []string{testPolicyName}, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + entityID := resp.Data["id"].(string) + + // Find mount accessor + resp, err = core.systemBackend.HandleRequest(namespace.RootContext(nil), &logical.Request{ + Path: "auth", + Operation: logical.ReadOperation, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\nerr: %v", resp, err) + } + tokenMountAccessor := resp.Data["token/"].(map[string]interface{})["accessor"].(string) + + // Create manually an entity alias + resp, err = i.HandleRequest(ctx, &logical.Request{ + Path: "entity-alias", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "name": entityAliasName, + "canonical_id": entityID, + "mount_accessor": tokenMountAccessor, + }, + }) + + // Create token role + resp, err = core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/roles/" + testRoleName, + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "period": "72h", + "allowed_entity_aliases": []string{"test1", "test2", "testentityaliasn"}, + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + + resp, _ = core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/create", + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "entity_alias": entityAliasName, + }, + }) + if resp == nil || resp.Data == nil { + t.Fatal("expected a response") + } + if resp.Data["error"] != "invalid 'entity_alias' value" { + t.Fatalf("wrong error returned. Err: %s", resp.Data["error"]) + } +} + +func TestTokenStore_HandleRequest_CreateToken_NoRoleEntityAlias(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + entityAliasName := "testentityalias" + + resp, _ := core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/create", + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "entity_alias": entityAliasName, + }, + }) + if resp == nil || resp.Data == nil { + t.Fatal("expected a response") + } + if resp.Data["error"] != "'entity_alias' is only allowed in combination with token role" { + t.Fatalf("wrong error returned. Err: %s", resp.Data["error"]) + } +} + func TestTokenStore_HandleRequest_RenewSelf(t *testing.T) { exp := mockExpiration(t) ts := exp.tokenStore