Token identity support (#6267)

* Implemented token backend support for identity

* Fixed tests

* Refactored a few checks for the token entity overwrite. Fixed tests.

* Moved entity alias check up so that the entity and entity alias is only created when it has been specified in allowed_entity_aliases list

* go mod vendor

* Added glob pattern

* Optimized allowed entity alias check

* Added test for asterisk only

* Changed to glob pattern anywhere

* Changed response code in case of failure. Changed globbing pattern check. Added docs.

* Added missing token role get parameter. Added more samples

* Fixed failing tests

* Corrected some cosmetical review points

* Changed response code for invalid provided entity alias

* Fixed minor things

* Fixed failing test
This commit is contained in:
Michel Vocks 2019-07-01 11:39:54 +02:00 committed by GitHub
parent e18866498d
commit 2b5aca4300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 456 additions and 23 deletions

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -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" {

View File

@ -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