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..3192cd9f282a 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 1eb14b0ee3a7..0cd0500018af 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -7,20 +7,18 @@ import ( "errors" "fmt" "net/http" - "sync" - "sync/atomic" - "regexp" "strings" + "sync" + "sync/atomic" "time" - proto "github.com/golang/protobuf/proto" + "github.com/armon/go-metrics" + "github.com/golang/protobuf/proto" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" - sockaddr "github.com/hashicorp/go-sockaddr" - - metrics "github.com/armon/go-metrics" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/framework" @@ -397,6 +395,11 @@ func (ts *TokenStore) paths() []*framework.Path { Description: "Use 'token_bound_cidrs' instead.", Deprecated: true, }, + + "allowed_entity_aliases": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: "String or JSON list of allowed entity aliases. If set, specifies the entity aliases which are allowed to be used during token generation. This field supports globbing.", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -611,6 +614,9 @@ type tsRoleEntry struct { // The set of CIDRs that tokens generated using this role will be bound to BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` + + // 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 { @@ -1819,11 +1825,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) @@ -2106,6 +2112,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque NumUses int `mapstructure:"num_uses"` Period string Type string `mapstructure:"type"` + EntityAlias string `mapstructure:"entity_alias"` } if err := mapstructure.WeakDecode(req.Data, &data); err != nil { return logical.ErrorResponse(fmt.Sprintf( @@ -2202,6 +2209,51 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque logical.ErrInvalidRequest } + // Verify the entity alias + var explicitEntityID string + 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 + } + + // Check if there is a concrete match + if !strutil.StrListContains(role.AllowedEntityAliases, data.EntityAlias) && + !strutil.StrListContainsGlob(role.AllowedEntityAliases, data.EntityAlias) { + return logical.ErrorResponse("invalid 'entity_alias' value"), logical.ErrInvalidRequest + } + + // Get mount accessor which is required to lookup entity alias + mountValidationResp := ts.core.router.MatchingMountByAccessor(req.MountAccessor) + if mountValidationResp == nil { + return logical.ErrorResponse("auth token mount accessor not found"), nil + } + + // Create alias for later processing + alias := &logical.Alias{ + Name: data.EntityAlias, + MountAccessor: mountValidationResp.Accessor, + MountType: mountValidationResp.Type, + } + + // Create or fetch entity from entity alias + entity, err := ts.core.identityStore.CreateOrFetchEntity(ctx, alias) + if err != nil { + return nil, err + } + if entity == nil { + return nil, errors.New("failed to create or fetch entity from given entity alias") + } + + // Validate that the entity is not disabled + if entity.Disabled { + return logical.ErrorResponse("entity from given entity alias is disabled"), logical.ErrPermissionDenied + } + + // Set new entity id + explicitEntityID = entity.ID + } + // Setup the token entry te := logical.TokenEntry{ Parent: req.ClientToken, @@ -2434,9 +2486,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 explicitEntityID != "": + // Overwrite the entity identifier + te.EntityID = explicitEntityID + case te.Parent != "": te.EntityID = parent.EntityID // If the parent has bound CIDRs, copy those into the child. We don't @@ -2978,6 +3035,7 @@ func (ts *TokenStore) tokenStoreRoleRead(ctx context.Context, req *logical.Reque "path_suffix": role.PathSuffix, "renewable": role.Renewable, "token_type": role.TokenType.String(), + "allowed_entity_aliases": role.AllowedEntityAliases, }, } @@ -3183,6 +3241,11 @@ 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) + } + 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 b6df807af278..d03975d30b5d 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -13,17 +13,18 @@ import ( "testing" "time" - "github.com/hashicorp/go-sockaddr" - "github.com/go-test/deep" "github.com/hashicorp/errwrap" - hclog "github.com/hashicorp/go-hclog" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/locksutil" "github.com/hashicorp/vault/sdk/helper/parseutil" "github.com/hashicorp/vault/sdk/helper/tokenutil" "github.com/hashicorp/vault/sdk/logical" + "github.com/mitchellh/mapstructure" ) func TestTokenStore_CreateOrphanResponse(t *testing.T) { @@ -2614,6 +2615,342 @@ 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, + ClientToken: root, + 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/" + testRoleName, + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "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, + ClientToken: root, + 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", entityAliasName}, + }, + }) + 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/" + testRoleName, + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "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 := resp.Data["aliases"].([]interface{}) + if len(aliases) != 1 { + t.Fatalf("expected only one alias but got %d; Aliases: %#v", len(aliases), aliases) + } + alias := &identity.Alias{} + if err := mapstructure.Decode(aliases[0], alias); err != nil { + t.Fatal(err) + } + + // Validate + if alias.Name != entityAliasName { + t.Fatalf("alias name should be '%s' but is '%s'", entityAliasName, alias.Name) + } +} + +func TestTokenStore_HandleRequest_CreateToken_GlobPatternWildcardEntityAlias(t *testing.T) { + core, _, root := TestCoreUnsealed(t) + i := core.identityStore + ctx := namespace.RootContext(nil) + testRoleName := "test" + + tests := []struct { + name string + globPattern string + aliasName string + }{ + { + name: "prefix-asterisk", + globPattern: "*-web", + aliasName: "department-web", + }, + { + name: "suffix-asterisk", + globPattern: "web-*", + aliasName: "web-department", + }, + { + name: "middle-asterisk", + globPattern: "web-*-web", + aliasName: "web-department-web", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create token role + resp, err := core.HandleRequest(ctx, &logical.Request{ + Path: "auth/token/roles/" + testRoleName, + ClientToken: root, + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "period": "72h", + "path_suffix": "happening", + "bound_cidrs": []string{"0.0.0.0/0"}, + "allowed_entity_aliases": []string{"test1", "test2", test.globPattern}, + }, + }) + 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/" + testRoleName, + Operation: logical.UpdateOperation, + ClientToken: root, + Data: map[string]interface{}{ + "entity_alias": test.aliasName, + }, + }) + 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 := resp.Data["aliases"].([]interface{}) + if len(aliases) != 1 { + t.Fatalf("expected only one alias but got %d; Aliases: %#v", len(aliases), aliases) + } + alias := &identity.Alias{} + if err := mapstructure.Decode(aliases[0], alias); err != nil { + t.Fatal(err) + } + + // Validate + if alias.Name != test.aliasName { + t.Fatalf("alias name should be '%s' but is '%s'", test.aliasName, alias.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(ctx, &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, + ClientToken: root, + 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/" + testRoleName, + 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 @@ -2719,6 +3056,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "renewable": true, "token_type": "default-service", "token_num_uses": 123, + "allowed_entity_aliases": []string(nil), } if resp.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "0.0.0.0/0" { @@ -2778,6 +3116,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "explicit_max_ttl": int64(288000), "renewable": false, "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if resp.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "0.0.0.0/0" { @@ -2827,6 +3166,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "token_period": int64(0), "renewable": false, "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if resp.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "0.0.0.0/0" { @@ -2876,6 +3216,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "token_period": int64(0), "renewable": false, "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if diff := deep.Equal(expected, resp.Data); diff != nil { @@ -3685,6 +4026,7 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { "explicit_max_ttl": int64(3600), "renewable": false, "token_type": "batch", + "allowed_entity_aliases": []string(nil), } if resp.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1" { @@ -3737,6 +4079,7 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { "explicit_max_ttl": int64(7200), "renewable": false, "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if resp.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1" { @@ -3788,6 +4131,7 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { "explicit_max_ttl": int64(0), "renewable": false, "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1" { @@ -3841,6 +4185,7 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { "explicit_max_ttl": int64(0), "renewable": false, "token_type": "service", + "allowed_entity_aliases": []string(nil), } if resp.Data["token_bound_cidrs"].([]*sockaddr.SockAddrMarshaler)[0].String() != "127.0.0.1" { diff --git a/website/source/api/auth/token/index.html.md b/website/source/api/auth/token/index.html.md index 05d18328a259..309d4749b7d7 100644 --- a/website/source/api/auth/token/index.html.md +++ b/website/source/api/auth/token/index.html.md @@ -102,6 +102,10 @@ during this call. - `period` `(string: "")` - If specified, the token will be periodic; it will have no maximum TTL (unless an "explicit-max-ttl" is also set) but every renewal will use the given period. Requires a root/sudo token to use. +- `entity_alias` `(string: "")` - Name of the entity alias to associate with + during token creation. Only works in combination with `role_name` 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. ### Sample Payload @@ -573,16 +577,20 @@ $ curl \ "lease_duration": 0, "renewable": false, "data": { - "allowed_policies": [ - "dev" + "allowed_entity_aliases": [ + "my-entity-alias" ], + "allowed_policies": [], "disallowed_policies": [], "explicit_max_ttl": 0, "name": "nomad", "orphan": false, "path_suffix": "", "period": 0, - "renewable": true + "renewable": true, + "token_explicit_max_ttl": 0, + "token_period": 0, + "token_type": "default-service" }, "warnings": null } @@ -682,6 +690,9 @@ tokens created against a role to be revoked using the be returned unless the client requests a `batch` type token at token creation time. If `default-batch`, `batch` tokens will be returned unless the client requests a `service` type token at token creation time. +- `allowed_entity_aliases` `(string: "", or list: [])` - String or JSON list + of allowed entity aliases. If set, specifies the entity aliases which are + allowed to be used during token generation. This field supports globbing. ### Sample Payload @@ -692,7 +703,8 @@ tokens created against a role to be revoked using the "name": "nomad", "orphan": false, "bound_cidrs": ["127.0.0.1/32", "128.252.0.0/16"], - "renewable": true + "renewable": true, + "allowed_entity_aliases": ["web-entity-alias", "app-entity-*"] ``` ### Sample Request