From 03da5f54d1e4fdf05897bbfff35dd778e6637cee Mon Sep 17 00:00:00 2001 From: Tiernan Messmer Date: Sat, 18 Sep 2021 18:20:22 +1000 Subject: [PATCH] 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 | 267 +++++++++++++++++------- website/content/api-docs/auth/token.mdx | 32 ++- 4 files changed, 229 insertions(+), 83 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..b462438bc7682 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 @@ -3737,7 +3668,6 @@ func TestTokenStore_RoleAllowedPolicies(t *testing.T) { req.ClientToken = root req.Data = map[string]interface{}{ "allowed_policies": "", - "allowed_policies_glob": "", } 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.