From a538936367ea2380dab5eb607c56d9b2fc5014ba Mon Sep 17 00:00:00 2001 From: Tiernan Date: Wed, 22 Sep 2021 01:25:06 +1000 Subject: [PATCH] Allow globbing dis/allowed_policies_glob in token roles (#7277) * Add allowed_policies_glob and disallowed_policies_glob that are the same as allowed_policies and disallowed_policies but allow glob matching. * Update changelog, docs, tests, and comments for (dis)allowed_token_glob token role feature. * Improve docs and unit tests for auth/token role policy globbing. --- changelog/7277.txt | 3 + vault/token_store.go | 89 ++++- vault/token_store_test.go | 466 +++++++++++++++++++----- website/content/api-docs/auth/token.mdx | 34 +- 4 files changed, 468 insertions(+), 124 deletions(-) create mode 100644 changelog/7277.txt diff --git a/changelog/7277.txt b/changelog/7277.txt new file mode 100644 index 000000000..4a19cf98e --- /dev/null +++ b/changelog/7277.txt @@ -0,0 +1,3 @@ +```release-note:feature +auth/token: Add `allowed_policies_glob` and `disallowed_policies_glob` fields to token roles to allow glob matching of policies +``` diff --git a/vault/token_store.go b/vault/token_store.go index 3f2435f06..0fd011a01 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -392,6 +392,16 @@ func (ts *TokenStore) paths() []*framework.Path { Description: tokenDisallowedPoliciesHelp, }, + "allowed_policies_glob": { + Type: framework.TypeCommaStringSlice, + Description: tokenAllowedPoliciesGlobHelp, + }, + + "disallowed_policies_glob": { + Type: framework.TypeCommaStringSlice, + Description: tokenDisallowedPoliciesGlobHelp, + }, + "orphan": { Type: framework.TypeBool, Description: tokenOrphanHelp, @@ -623,6 +633,12 @@ type tsRoleEntry struct { // List of policies to be not allowed during token creation using this role DisallowedPolicies []string `json:"disallowed_policies" mapstructure:"disallowed_policies" structs:"disallowed_policies"` + // An extension to AllowedPolicies that instead uses glob matching on policy names + AllowedPoliciesGlob []string `json:"allowed_policies_glob" mapstructure:"allowed_policies_glob" structs:"allowed_policies_glob"` + + // An extension to DisallowedPolicies that instead uses glob matching on policy names + DisallowedPoliciesGlob []string `json:"disallowed_policies_glob" mapstructure:"disallowed_policies_glob" structs:"disallowed_policies_glob"` + // If true, tokens created using this role will be orphans Orphan bool `json:"orphan" mapstructure:"orphan" structs:"orphan"` @@ -2475,7 +2491,8 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque // and shouldn't be added is kept because we want to do subset comparisons // based on adding default when it's correct to do so. switch { - case role != nil && (len(role.AllowedPolicies) > 0 || len(role.DisallowedPolicies) > 0): + case role != nil && (len(role.AllowedPolicies) > 0 || len(role.DisallowedPolicies) > 0 || + len(role.AllowedPoliciesGlob) > 0 || len(role.DisallowedPoliciesGlob) > 0): // Holds the final set of policies as they get munged var finalPolicies []string @@ -2487,7 +2504,9 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque // isn't in the disallowed list, add it. This is in line with the idea // that roles, when allowed/disallowed ar set, allow a subset of // policies to be set disjoint from the parent token's policies. - if !data.NoDefaultPolicy && !role.TokenNoDefaultPolicy && !strutil.StrListContains(role.DisallowedPolicies, "default") { + if !data.NoDefaultPolicy && !role.TokenNoDefaultPolicy && + !strutil.StrListContains(role.DisallowedPolicies, "default") && + !strutil.StrListContainsGlob(role.DisallowedPoliciesGlob, "default") { localAddDefault = true } @@ -2496,12 +2515,12 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque finalPolicies = policyutil.SanitizePolicies(data.Policies, localAddDefault) } - var sanitizedRolePolicies []string + var sanitizedRolePolicies, sanitizedRolePoliciesGlob []string // First check allowed policies; if policies are specified they will be // checked, otherwise if an allowed set exists that will be the set // that is used - if len(role.AllowedPolicies) > 0 { + if len(role.AllowedPolicies) > 0 || len(role.AllowedPoliciesGlob) > 0 { // Note that if "default" is already in allowed, and also in // disallowed, this will still result in an error later since this // doesn't strip out default @@ -2510,8 +2529,13 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque if len(finalPolicies) == 0 { finalPolicies = sanitizedRolePolicies } else { - if !strutil.StrListSubset(sanitizedRolePolicies, finalPolicies) { - return logical.ErrorResponse(fmt.Sprintf("token policies (%q) must be subset of the role's allowed policies (%q)", finalPolicies, sanitizedRolePolicies)), logical.ErrInvalidRequest + sanitizedRolePoliciesGlob = policyutil.SanitizePolicies(role.AllowedPoliciesGlob, false) + + for _, finalPolicy := range finalPolicies { + if !strutil.StrListContains(sanitizedRolePolicies, finalPolicy) && + !strutil.StrListContainsGlob(sanitizedRolePoliciesGlob, finalPolicy) { + return logical.ErrorResponse(fmt.Sprintf("token policies (%q) must be subset of the role's allowed policies (%q) or glob policies (%q)", finalPolicies, sanitizedRolePolicies, sanitizedRolePoliciesGlob)), logical.ErrInvalidRequest + } } } } else { @@ -2522,12 +2546,14 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque } } - if len(role.DisallowedPolicies) > 0 { + if len(role.DisallowedPolicies) > 0 || len(role.DisallowedPoliciesGlob) > 0 { // We don't add the default here because we only want to disallow it if it's explicitly set sanitizedRolePolicies = strutil.RemoveDuplicates(role.DisallowedPolicies, true) + sanitizedRolePoliciesGlob = strutil.RemoveDuplicates(role.DisallowedPoliciesGlob, true) for _, finalPolicy := range finalPolicies { - if strutil.StrListContains(sanitizedRolePolicies, finalPolicy) { + if strutil.StrListContains(sanitizedRolePolicies, finalPolicy) || + strutil.StrListContainsGlob(sanitizedRolePoliciesGlob, finalPolicy) { return logical.ErrorResponse(fmt.Sprintf("token policy %q is disallowed by this role", finalPolicy)), logical.ErrInvalidRequest } } @@ -3183,18 +3209,20 @@ func (ts *TokenStore) tokenStoreRoleRead(ctx context.Context, req *logical.Reque // TODO (1.4): Remove "period" and "explicit_max_ttl" if they're zero resp := &logical.Response{ Data: map[string]interface{}{ - "period": int64(role.Period.Seconds()), - "token_period": int64(role.TokenPeriod.Seconds()), - "explicit_max_ttl": int64(role.ExplicitMaxTTL.Seconds()), - "token_explicit_max_ttl": int64(role.TokenExplicitMaxTTL.Seconds()), - "disallowed_policies": role.DisallowedPolicies, - "allowed_policies": role.AllowedPolicies, - "name": role.Name, - "orphan": role.Orphan, - "path_suffix": role.PathSuffix, - "renewable": role.Renewable, - "token_type": role.TokenType.String(), - "allowed_entity_aliases": role.AllowedEntityAliases, + "period": int64(role.Period.Seconds()), + "token_period": int64(role.TokenPeriod.Seconds()), + "explicit_max_ttl": int64(role.ExplicitMaxTTL.Seconds()), + "token_explicit_max_ttl": int64(role.TokenExplicitMaxTTL.Seconds()), + "disallowed_policies": role.DisallowedPolicies, + "allowed_policies": role.AllowedPolicies, + "disallowed_policies_glob": role.DisallowedPoliciesGlob, + "allowed_policies_glob": role.AllowedPoliciesGlob, + "name": role.Name, + "orphan": role.Orphan, + "path_suffix": role.PathSuffix, + "renewable": role.Renewable, + "token_type": role.TokenType.String(), + "allowed_entity_aliases": role.AllowedEntityAliases, }, } @@ -3292,6 +3320,20 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(ctx context.Context, req *logic } else if req.Operation == logical.CreateOperation { entry.DisallowedPolicies = strutil.RemoveDuplicates(data.Get("disallowed_policies").([]string), true) } + + allowedPoliciesGlobRaw, ok := data.GetOk("allowed_policies_glob") + if ok { + entry.AllowedPoliciesGlob = policyutil.SanitizePolicies(allowedPoliciesGlobRaw.([]string), policyutil.DoNotAddDefaultPolicy) + } else if req.Operation == logical.CreateOperation { + entry.AllowedPoliciesGlob = policyutil.SanitizePolicies(data.Get("allowed_policies_glob").([]string), policyutil.DoNotAddDefaultPolicy) + } + + disallowedPoliciesGlobRaw, ok := data.GetOk("disallowed_policies_glob") + if ok { + entry.DisallowedPoliciesGlob = strutil.RemoveDuplicates(disallowedPoliciesGlobRaw.([]string), true) + } else if req.Operation == logical.CreateOperation { + entry.DisallowedPoliciesGlob = strutil.RemoveDuplicates(data.Get("disallowed_policies_glob").([]string), true) + } } // We handle token type a bit differently than tokenutil does so we need to @@ -3779,6 +3821,13 @@ calling token's policies. The parameter is a comma-delimited string of policy names.` tokenDisallowedPoliciesHelp = `If set, successful token creation via this role will require that no policies in the given list are requested. The parameter is a comma-delimited string of policy names.` + tokenAllowedPoliciesGlobHelp = `If set, tokens can be created with any subset of glob matched policies in this +list, rather than the normal semantics of tokens being a subset of the +calling token's policies. The parameter is a comma-delimited string of +policy name globs.` + tokenDisallowedPoliciesGlobHelp = `If set, successful token creation via this role will require that +no requested policies glob match any of policies in this list. +The parameter is a comma-delimited string of policy name globs.` tokenOrphanHelp = `If true, tokens created via this role will be orphan tokens (have no parent)` tokenPeriodHelp = `If set, tokens created via this role diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 6a5504e16..4a4bf5557 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -3179,19 +3179,21 @@ func TestTokenStore_RoleCRUD(t *testing.T) { } expected := map[string]interface{}{ - "name": "test", - "orphan": true, - "token_period": int64(259200), - "period": int64(259200), - "allowed_policies": []string{"test1", "test2"}, - "disallowed_policies": []string{}, - "path_suffix": "happenin", - "explicit_max_ttl": int64(7200), - "token_explicit_max_ttl": int64(7200), - "renewable": true, - "token_type": "default-service", - "token_num_uses": 123, - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": true, + "token_period": int64(259200), + "period": int64(259200), + "allowed_policies": []string{"test1", "test2"}, + "disallowed_policies": []string{}, + "allowed_policies_glob": []string{}, + "disallowed_policies_glob": []string{}, + "path_suffix": "happenin", + "explicit_max_ttl": int64(7200), + "token_explicit_max_ttl": int64(7200), + "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" { @@ -3240,18 +3242,20 @@ func TestTokenStore_RoleCRUD(t *testing.T) { } expected = map[string]interface{}{ - "name": "test", - "orphan": true, - "period": int64(284400), - "token_period": int64(284400), - "allowed_policies": []string{"test3"}, - "disallowed_policies": []string{}, - "path_suffix": "happenin", - "token_explicit_max_ttl": int64(288000), - "explicit_max_ttl": int64(288000), - "renewable": false, - "token_type": "default-service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": true, + "period": int64(284400), + "token_period": int64(284400), + "allowed_policies": []string{"test3"}, + "disallowed_policies": []string{}, + "allowed_policies_glob": []string{}, + "disallowed_policies_glob": []string{}, + "path_suffix": "happenin", + "token_explicit_max_ttl": int64(288000), + "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" { @@ -3290,18 +3294,20 @@ func TestTokenStore_RoleCRUD(t *testing.T) { } expected = map[string]interface{}{ - "name": "test", - "orphan": true, - "explicit_max_ttl": int64(5), - "token_explicit_max_ttl": int64(5), - "allowed_policies": []string{"test3"}, - "disallowed_policies": []string{}, - "path_suffix": "happenin", - "period": int64(0), - "token_period": int64(0), - "renewable": false, - "token_type": "default-service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": true, + "explicit_max_ttl": int64(5), + "token_explicit_max_ttl": int64(5), + "allowed_policies": []string{"test3"}, + "disallowed_policies": []string{}, + "allowed_policies_glob": []string{}, + "disallowed_policies_glob": []string{}, + "path_suffix": "happenin", + "period": int64(0), + "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" { @@ -3340,18 +3346,20 @@ func TestTokenStore_RoleCRUD(t *testing.T) { } expected = map[string]interface{}{ - "name": "test", - "orphan": true, - "token_explicit_max_ttl": int64(5), - "explicit_max_ttl": int64(5), - "allowed_policies": []string{"test3"}, - "disallowed_policies": []string{}, - "path_suffix": "", - "period": int64(0), - "token_period": int64(0), - "renewable": false, - "token_type": "default-service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": true, + "token_explicit_max_ttl": int64(5), + "explicit_max_ttl": int64(5), + "allowed_policies": []string{"test3"}, + "disallowed_policies": []string{}, + "allowed_policies_glob": []string{}, + "disallowed_policies_glob": []string{}, + "path_suffix": "", + "period": int64(0), + "token_period": int64(0), + "renewable": false, + "token_type": "default-service", + "allowed_entity_aliases": []string(nil), } if diff := deep.Equal(expected, resp.Data); diff != nil { @@ -3495,6 +3503,17 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { t.Fatalf("err:%v resp:%v", err, resp) } + // policy containing a glob character in the non-glob disallowed_policies field + req = logical.TestRequest(t, logical.UpdateOperation, "roles/testglobdisabled") + req.ClientToken = root + req.Data = map[string]interface{}{ + "disallowed_policies": "test*", + } + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%v", err, resp) + } + // Create a token that has all the policies defined above req = logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = root @@ -3508,6 +3527,7 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { } parentToken := resp.Auth.ClientToken + // Test that the parent token's policies are rejected by disallowed_policies req = logical.TestRequest(t, logical.UpdateOperation, "create/test1") req.ClientToken = parentToken resp, err = ts.HandleRequest(namespace.RootContext(nil), req) @@ -3536,6 +3556,21 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { req.ClientToken = parentToken testMakeTokenViaRequest(t, ts, req) + // Check to be sure 'test*' without globbing matches 'test*' + req = logical.TestRequest(t, logical.UpdateOperation, "create/testglobdisabled") + req.Data["policies"] = []string{"test*"} + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatalf("expected an error response, got %#v", resp) + } + + // Check to be sure 'test*' without globbing doesn't match 'test1' or 'test' + req = logical.TestRequest(t, logical.UpdateOperation, "create/testglobdisabled") + req.Data["policies"] = []string{"test1", "test"} + req.ClientToken = parentToken + testMakeTokenViaRequest(t, ts, req) + // Create a role to have 'default' policy disallowed req = logical.TestRequest(t, logical.UpdateOperation, "roles/default") req.ClientToken = root @@ -3588,6 +3623,40 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { t.Fatalf("bad: %#v", resp) } + // test not glob matching when using allowed_policies instead of allowed_policies_glob + req = logical.TestRequest(t, logical.UpdateOperation, "roles/testnoglob") + req.ClientToken = root + req.Data = map[string]interface{}{ + "allowed_policies": "test*", + } + + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + if resp != nil { + t.Fatalf("expected a nil response") + } + + req.Path = "create/testnoglob" + req.Data["policies"] = []string{"test"} + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil { + t.Fatalf("expected error") + } + + req.Data["policies"] = []string{"testfoo"} + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil { + t.Fatalf("expected error") + } + + req.Data["policies"] = []string{"test*"} + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + // When allowed_policies is blank, should fall back to a subset of the parent policies req = logical.TestRequest(t, logical.UpdateOperation, "roles/test") req.ClientToken = root @@ -3643,6 +3712,201 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { } } +func TestTokenStore_RoleDisallowedPoliciesGlob(t *testing.T) { + var req *logical.Request + var resp *logical.Response + var err error + + core, _, root := TestCoreUnsealed(t) + ts := core.tokenStore + ps := core.policyStore + + // Create 4 different policies + policy, _ := ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) + policy.Name = "test1" + if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { + t.Fatal(err) + } + + policy, _ = ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) + policy.Name = "test2" + if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { + t.Fatal(err) + } + + policy, _ = ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) + policy.Name = "test3" + if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { + t.Fatal(err) + } + + policy, _ = ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) + policy.Name = "test3b" + if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { + t.Fatal(err) + } + + // Create roles with different disallowed_policies configuration + req = logical.TestRequest(t, logical.UpdateOperation, "roles/test1") + req.ClientToken = root + req.Data = map[string]interface{}{ + "disallowed_policies_glob": "test1", + } + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%v", err, resp) + } + + req = logical.TestRequest(t, logical.UpdateOperation, "roles/testnot23") + req.ClientToken = root + req.Data = map[string]interface{}{ + "disallowed_policies_glob": "test2,test3*", + } + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%v", err, resp) + } + + // Create a token that has all the policies defined above + req = logical.TestRequest(t, logical.UpdateOperation, "create") + req.ClientToken = root + req.Data["policies"] = []string{"test1", "test2", "test3", "test3b"} + resp = testMakeTokenViaRequest(t, ts, req) + if resp == nil || resp.Auth == nil { + t.Fatal("got nil response") + } + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: ClientToken; resp:%#v", resp) + } + parentToken := resp.Auth.ClientToken + + // Test that the parent token's policies are rejected by disallowed_policies + req = logical.TestRequest(t, logical.UpdateOperation, "create/test1") + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatalf("expected an error response, got %#v", resp) + } + req = logical.TestRequest(t, logical.UpdateOperation, "create/testnot23") + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatalf("expected an error response, got %#v", resp) + } + + // Disallowed should act as a blacklist so make sure we can still make + // something with other policies in the request + req = logical.TestRequest(t, logical.UpdateOperation, "create/test1") + req.Data["policies"] = []string{"foo", "bar"} + req.ClientToken = parentToken + testMakeTokenViaRequest(t, ts, req) + + // Check to be sure 'test3*' matches 'test3' + req = logical.TestRequest(t, logical.UpdateOperation, "create/testnot23") + req.Data["policies"] = []string{"test3"} + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatalf("expected an error response, got %#v", resp) + } + + // Check to be sure 'test3*' matches 'test3b' + req = logical.TestRequest(t, logical.UpdateOperation, "create/testnot23") + req.Data["policies"] = []string{"test3b"} + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatalf("expected an error response, got %#v", resp) + } + + // Check that non-blacklisted policies still work + req = logical.TestRequest(t, logical.UpdateOperation, "create/testnot23") + req.Data["policies"] = []string{"test1"} + req.ClientToken = parentToken + testMakeTokenViaRequest(t, ts, req) + + // Create a role to have 'default' policy disallowed + req = logical.TestRequest(t, logical.UpdateOperation, "roles/default") + req.ClientToken = root + req.Data = map[string]interface{}{ + "disallowed_policies_glob": "default", + } + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%v", err, resp) + } + + req = logical.TestRequest(t, logical.UpdateOperation, "create/default") + req.ClientToken = parentToken + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil || resp != nil && !resp.IsError() { + t.Fatal("expected an error response") + } +} + +func TestTokenStore_RoleAllowedPoliciesGlob(t *testing.T) { + c, _, root := TestCoreUnsealed(t) + ts := c.tokenStore + + // test literal matching works in allowed_policies_glob + req := logical.TestRequest(t, logical.UpdateOperation, "roles/test") + req.ClientToken = root + req.Data = map[string]interface{}{ + "allowed_policies_glob": "test1,test2", + } + + resp, err := ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + if resp != nil { + t.Fatalf("expected a nil response") + } + + req.Data = map[string]interface{}{} + + req.Path = "create/test" + req.Data["policies"] = []string{"foo"} + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil { + t.Fatalf("expected error") + } + + req.Data["policies"] = []string{"test2"} + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + + // test glob matching in allowed_policies_glob + req = logical.TestRequest(t, logical.UpdateOperation, "roles/test") + req.ClientToken = root + req.Data = map[string]interface{}{ + "allowed_policies_glob": "test*", + } + + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + if resp != nil { + t.Fatalf("expected a nil response") + } + + req.Path = "create/test" + req.Data["policies"] = []string{"footest"} + resp, err = ts.HandleRequest(namespace.RootContext(nil), req) + if err == nil { + t.Fatalf("expected error") + } + + req.Data["policies"] = []string{"testfoo", "test2", "test"} + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } +} + func TestTokenStore_RoleOrphan(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore @@ -4150,18 +4414,20 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { } expected := map[string]interface{}{ - "name": "test", - "orphan": false, - "period": int64(1), - "token_period": int64(1), - "allowed_policies": []string(nil), - "disallowed_policies": []string(nil), - "path_suffix": "", - "token_explicit_max_ttl": int64(3600), - "explicit_max_ttl": int64(3600), - "renewable": false, - "token_type": "batch", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": false, + "period": int64(1), + "token_period": int64(1), + "allowed_policies": []string(nil), + "disallowed_policies": []string(nil), + "allowed_policies_glob": []string(nil), + "disallowed_policies_glob": []string(nil), + "path_suffix": "", + "token_explicit_max_ttl": int64(3600), + "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" { @@ -4203,18 +4469,20 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { } expected := map[string]interface{}{ - "name": "test", - "orphan": false, - "period": int64(5), - "token_period": int64(5), - "allowed_policies": []string(nil), - "disallowed_policies": []string(nil), - "path_suffix": "", - "token_explicit_max_ttl": int64(7200), - "explicit_max_ttl": int64(7200), - "renewable": false, - "token_type": "default-service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": false, + "period": int64(5), + "token_period": int64(5), + "allowed_policies": []string(nil), + "disallowed_policies": []string(nil), + "allowed_policies_glob": []string(nil), + "disallowed_policies_glob": []string(nil), + "path_suffix": "", + "token_explicit_max_ttl": int64(7200), + "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" { @@ -4255,18 +4523,20 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { } expected := map[string]interface{}{ - "name": "test", - "orphan": false, - "period": int64(0), - "token_period": int64(7), - "allowed_policies": []string(nil), - "disallowed_policies": []string(nil), - "path_suffix": "", - "token_explicit_max_ttl": int64(5200), - "explicit_max_ttl": int64(0), - "renewable": false, - "token_type": "default-service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": false, + "period": int64(0), + "token_period": int64(7), + "allowed_policies": []string(nil), + "disallowed_policies": []string(nil), + "allowed_policies_glob": []string(nil), + "disallowed_policies_glob": []string(nil), + "path_suffix": "", + "token_explicit_max_ttl": int64(5200), + "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" { @@ -4309,18 +4579,20 @@ func TestTokenStore_RoleTokenFields(t *testing.T) { } expected := map[string]interface{}{ - "name": "test", - "orphan": false, - "period": int64(0), - "token_period": int64(5), - "allowed_policies": []string(nil), - "disallowed_policies": []string(nil), - "path_suffix": "", - "token_explicit_max_ttl": int64(7200), - "explicit_max_ttl": int64(0), - "renewable": false, - "token_type": "service", - "allowed_entity_aliases": []string(nil), + "name": "test", + "orphan": false, + "period": int64(0), + "token_period": int64(5), + "allowed_policies": []string(nil), + "disallowed_policies": []string(nil), + "allowed_policies_glob": []string(nil), + "disallowed_policies_glob": []string(nil), + "path_suffix": "", + "token_explicit_max_ttl": int64(7200), + "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/content/api-docs/auth/token.mdx b/website/content/api-docs/auth/token.mdx index 48dd4e955..8bee69c6b 100644 --- a/website/content/api-docs/auth/token.mdx +++ b/website/content/api-docs/auth/token.mdx @@ -73,7 +73,7 @@ during this call. - `role_name` `(string: "")` – The name of the token role. - `policies` `(array: "")` – A list of policies for the token. This must be a - subset of the policies belonging to the token making the request, unless + subset of the policies belonging to the token making the request, unless the calling token is root or contains `sudo` capabilities to `auth/token/create`. If not specified, defaults to all the policies of the calling token. - `meta` `(map: {})` – A map of string to string valued metadata. This is @@ -338,8 +338,8 @@ if there is a lease associated with it. - `token` `(string: )` - Token to renew. This can be part of the URL or the body. - `increment` `(string: "")` - An optional requested increment duration can be - provided. This increment may not be honored, for instance in the case of periodic tokens. - If not supplied, Vault will use the default TTL. This is specified as a numeric string + provided. This increment may not be honored, for instance in the case of periodic tokens. + If not supplied, Vault will use the default TTL. This is specified as a numeric string with suffix like "30s" or "5m". ### Sample Payload @@ -389,8 +389,8 @@ possible only if there is a lease associated with it. ### Parameters - `increment` `(string: "")` - An optional requested increment duration can be - provided. This increment may not be honored, for instance in the case of periodic tokens. - If not supplied, Vault will use the default TTL. This is specified as a numeric string + provided. This increment may not be honored, for instance in the case of periodic tokens. + If not supplied, Vault will use the default TTL. This is specified as a numeric string with suffix like "30s" or "5m". ### Sample Payload @@ -627,6 +627,8 @@ $ curl \ ], "allowed_policies": [], "disallowed_policies": [], + "allowed_policies_glob": [], + "disallowed_policies_glob": [], "explicit_max_ttl": 0, "name": "nomad", "orphan": false, @@ -690,13 +692,31 @@ tokens created against a role to be revoked using the tokens being a subset of the calling token's policies. The parameter is a comma-delimited string of policy names. If at creation time `no_default_policy` is not set and `"default"` is not contained in - `disallowed_policies`, the `"default"` policy will be added to the created - token automatically. + `disallowed_policies` or glob matched in `disallowed_policies_glob`, + the `"default"` policy will be added to the created token automatically. - `disallowed_policies` `(list: [])` – If set, successful token creation via this role will require that no policies in the given list are requested. The parameter is a comma-delimited string of policy names. Adding `"default"` to this list will prevent `"default"` from being added automatically to created tokens. +- `allowed_policies_glob` `(list: [])` – If set, tokens can be created with any + subset of glob matched policies in this list, rather than the normal semantics + of tokens being a subset of the calling token's policies. The parameter is a + comma-delimited string of policy name globs. If at creation time + `no_default_policy` is not set and `"default"` is not contained in + `disallowed_policies` or glob matched in `disallowed_policies_glob`, + the `"default"` policy will be added to the created token automatically. + If combined with `allowed_policies` policies need to only match one of the two + lists to be permitted. Note that unlike `allowed_policies` the policies listed + in `allowed_policies_glob` will not be added to the token when no policies are + specified in the call to `/auth/token/create/:role_name`. +- `disallowed_policies_glob` `(list: [])` – If set, successful token creation via + this role will require that no requested policies glob match any of policies in + this list. The parameter is a comma-delimited string of policy name globs. + Adding any glob that matches `"default"` to this list will prevent `"default"` + from being added automatically to created tokens. + If combined with `disallowed_policies` policies need to only match one of the + two lists to be blocked. - `orphan` `(bool: false)` - If `true`, tokens created against this policy will be orphan tokens (they will have no parent). As such, they will not be automatically revoked by the revocation of any other token.