From cedd6c93fdb2f2b4df04eda78acfeb02d345c485 Mon Sep 17 00:00:00 2001 From: Tiernan Messmer Date: Fri, 17 Sep 2021 17:19:20 +1000 Subject: [PATCH 1/3] Add allowed_policies_glob and disallowed_policies_glob that are the same as allowed_policies and disallowed_policies but allow glob matching. --- vault/token_store.go | 89 +++++++--- vault/token_store_test.go | 353 +++++++++++++++++++++++++++----------- 2 files changed, 322 insertions(+), 120 deletions(-) diff --git a/vault/token_store.go b/vault/token_store.go index 3f2435f061a5d..f13180689a9df 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,13 @@ 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"` + // The policies that creation functions using this role can assign to a token, + // escaping or further locking down normal subset checking + AllowedPoliciesGlob []string `json:"allowed_policies_glob" mapstructure:"allowed_policies_glob" structs:"allowed_policies_glob"` + + // List of policies to be not allowed during token creation using this role + 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 +2492,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 +2505,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 +2516,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 +2530,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 +2547,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 +3210,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 +3321,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 +3822,12 @@ 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 the 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 policies in the given list are requested. 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 6a5504e165bfc..e8020d09c5a10 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 { @@ -3445,7 +3453,7 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { ts := core.tokenStore ps := core.policyStore - // Create 3 different policies + // Create 4 different policies policy, _ := ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) policy.Name = "test1" if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { @@ -3464,6 +3472,12 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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 @@ -3495,10 +3509,31 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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) + } + + // 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 - req.Data["policies"] = []string{"test1", "test2", "test3"} + req.Data["policies"] = []string{"test1", "test2", "test3", "test3b"} resp = testMakeTokenViaRequest(t, ts, req) if resp == nil || resp.Auth == nil { t.Fatal("got nil response") @@ -3529,6 +3564,13 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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/test123") @@ -3536,6 +3578,45 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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 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) + + // 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 @@ -3588,11 +3669,75 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { t.Fatalf("bad: %#v", resp) } + // test glob matching with allowed_policies_glob + req = logical.TestRequest(t, logical.UpdateOperation, "roles/test") + req.ClientToken = root + req.Data = map[string]interface{}{ + "allowed_policies": "", + "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"} + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + 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 req.Data = map[string]interface{}{ - "allowed_policies": "", + "allowed_policies": "", + "allowed_policies_glob": "", } resp, err = ts.HandleRequest(namespace.RootContext(nil), req) if err != nil || (resp != nil && resp.IsError()) { @@ -4150,18 +4295,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 +4350,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 +4404,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 +4460,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" { From 285c92719111978d1887d7163590fd52fc746903 Mon Sep 17 00:00:00 2001 From: Tiernan Messmer Date: Sat, 18 Sep 2021 18:20:22 +1000 Subject: [PATCH 2/3] Update changelog, docs, tests, and comments for (dis)allowed_token_glob token role feature. --- changelog/7277.txt | 3 + vault/token_store.go | 10 +- vault/token_store_test.go | 269 +++++++++++++++++------- website/content/api-docs/auth/token.mdx | 32 ++- 4 files changed, 230 insertions(+), 84 deletions(-) create mode 100644 changelog/7277.txt diff --git a/changelog/7277.txt b/changelog/7277.txt new file mode 100644 index 0000000000000..4a19cf98e19cd --- /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 f13180689a9df..0fd011a016196 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -633,11 +633,10 @@ 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"` - // The policies that creation functions using this role can assign to a token, - // escaping or further locking down normal subset checking + // 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"` - // List of policies to be not allowed during token creation using this role + // 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 @@ -3822,12 +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 the policies in this + 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 policies in the given list are requested. The parameter is a comma-delimited string of policy name globs.` +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 e8020d09c5a10..9379ed3fb4dc6 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -3509,16 +3509,6 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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) - } - // policy containing a glob character in the non-glob disallowed_policies field req = logical.TestRequest(t, logical.UpdateOperation, "roles/testglobdisabled") req.ClientToken = root @@ -3543,6 +3533,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) @@ -3564,13 +3555,6 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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/test123") @@ -3578,24 +3562,6 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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 to be sure 'test*' without globbing matches 'test*' req = logical.TestRequest(t, logical.UpdateOperation, "create/testglobdisabled") req.Data["policies"] = []string{"test*"} @@ -3611,12 +3577,6 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { req.ClientToken = parentToken testMakeTokenViaRequest(t, ts, req) - // 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 @@ -3669,35 +3629,6 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { t.Fatalf("bad: %#v", resp) } - // test glob matching with allowed_policies_glob - req = logical.TestRequest(t, logical.UpdateOperation, "roles/test") - req.ClientToken = root - req.Data = map[string]interface{}{ - "allowed_policies": "", - "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"} - resp = testMakeTokenViaRequest(t, ts, req) - if resp.Auth.ClientToken == "" { - 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 @@ -3736,8 +3667,7 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { req = logical.TestRequest(t, logical.UpdateOperation, "roles/test") req.ClientToken = root req.Data = map[string]interface{}{ - "allowed_policies": "", - "allowed_policies_glob": "", + "allowed_policies": "", } resp, err = ts.HandleRequest(namespace.RootContext(nil), req) if err != nil || (resp != nil && resp.IsError()) { @@ -3788,6 +3718,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 diff --git a/website/content/api-docs/auth/token.mdx b/website/content/api-docs/auth/token.mdx index 48dd4e9556157..de26b1c65f678 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,29 @@ 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. +- `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. From 4c90f1f8bb8eaa1b8e970494ca1f2b2c368d9130 Mon Sep 17 00:00:00 2001 From: Tiernan Messmer Date: Tue, 21 Sep 2021 10:37:59 +1000 Subject: [PATCH 3/3] Improve docs and unit tests for auth/token role policy globbing. --- vault/token_store_test.go | 10 ++-------- website/content/api-docs/auth/token.mdx | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 9379ed3fb4dc6..4a4bf55578754 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -3453,7 +3453,7 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { ts := core.tokenStore ps := core.policyStore - // Create 4 different policies + // Create 3 different policies policy, _ := ParseACLPolicy(namespace.RootNamespace, tokenCreationPolicy) policy.Name = "test1" if err := ps.SetPolicy(namespace.RootContext(nil), policy); err != nil { @@ -3472,12 +3472,6 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { 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 @@ -3523,7 +3517,7 @@ func TestTokenStore_RoleDisallowedPolicies(t *testing.T) { // 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"} + req.Data["policies"] = []string{"test1", "test2", "test3"} resp = testMakeTokenViaRequest(t, ts, req) if resp == nil || resp.Auth == nil { t.Fatal("got nil response") diff --git a/website/content/api-docs/auth/token.mdx b/website/content/api-docs/auth/token.mdx index de26b1c65f678..8bee69c6be3cc 100644 --- a/website/content/api-docs/auth/token.mdx +++ b/website/content/api-docs/auth/token.mdx @@ -707,7 +707,9 @@ tokens created against a role to be revoked using the `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. + 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.