diff --git a/helper/identity/templating.go b/helper/identity/templating.go new file mode 100644 index 0000000000..8342bdf677 --- /dev/null +++ b/helper/identity/templating.go @@ -0,0 +1,204 @@ +package identity + +import ( + "errors" + "strings" +) + +var ( + ErrUnbalancedTemplatingCharacter = errors.New("unbalanced templating characters") + ErrNoEntityAttachedToToken = errors.New("string contains entity template directives but no entity was provided") + ErrNoGroupsAttachedToToken = errors.New("string contains groups template directives but no groups were provided") + ErrTemplateValueNotFound = errors.New("no value could be found for one of the template directives") +) + +type PopulateStringInput struct { + ValidityCheckOnly bool + String string + Entity *Entity + Groups []*Group +} + +func PopulateString(p *PopulateStringInput) (bool, string, error) { + if p == nil { + return false, "", errors.New("nil input") + } + + if p.String == "" { + return false, "", nil + } + + var subst bool + splitStr := strings.Split(p.String, "{{") + + if len(splitStr) >= 1 { + if strings.Index(splitStr[0], "}}") != -1 { + return false, "", ErrUnbalancedTemplatingCharacter + } + if len(splitStr) == 1 { + return false, p.String, nil + } + } + + var b strings.Builder + if !p.ValidityCheckOnly { + b.Grow(2 * len(p.String)) + } + + for i, str := range splitStr { + if i == 0 { + if !p.ValidityCheckOnly { + b.WriteString(str) + } + continue + } + splitPiece := strings.Split(str, "}}") + switch len(splitPiece) { + case 2: + subst = true + if !p.ValidityCheckOnly { + tmplStr, err := performTemplating(strings.TrimSpace(splitPiece[0]), p.Entity, p.Groups) + if err != nil { + return false, "", err + } + b.WriteString(tmplStr) + b.WriteString(splitPiece[1]) + } + default: + return false, "", ErrUnbalancedTemplatingCharacter + } + } + + return subst, b.String(), nil +} + +func performTemplating(input string, entity *Entity, groups []*Group) (string, error) { + performAliasTemplating := func(trimmed string, alias *Alias) (string, error) { + switch { + case trimmed == "id": + return alias.ID, nil + case trimmed == "name": + if alias.Name == "" { + return "", ErrTemplateValueNotFound + } + return alias.Name, nil + case strings.HasPrefix(trimmed, "metadata."): + val, ok := alias.Metadata[strings.TrimPrefix(trimmed, "metadata.")] + if !ok { + return "", ErrTemplateValueNotFound + } + return val, nil + } + + return "", ErrTemplateValueNotFound + } + + performEntityTemplating := func(trimmed string) (string, error) { + switch { + case trimmed == "id": + return entity.ID, nil + case trimmed == "name": + if entity.Name == "" { + return "", ErrTemplateValueNotFound + } + return entity.Name, nil + case strings.HasPrefix(trimmed, "metadata."): + val, ok := entity.Metadata[strings.TrimPrefix(trimmed, "metadata.")] + if !ok { + return "", ErrTemplateValueNotFound + } + return val, nil + case strings.HasPrefix(trimmed, "aliases."): + split := strings.SplitN(strings.TrimPrefix(trimmed, "aliases."), ".", 2) + if len(split) != 2 { + return "", errors.New("invalid alias selector") + } + var found *Alias + for _, alias := range entity.Aliases { + if split[0] == alias.MountAccessor { + found = alias + break + } + } + if found == nil { + return "", errors.New("alias not found") + } + return performAliasTemplating(split[1], found) + } + + return "", ErrTemplateValueNotFound + } + + performGroupsTemplating := func(trimmed string) (string, error) { + var ids bool + + selectorSplit := strings.SplitN(trimmed, ".", 2) + switch { + case len(selectorSplit) != 2: + return "", errors.New("invalid groups selector") + case selectorSplit[0] == "ids": + ids = true + case selectorSplit[0] == "names": + default: + return "", errors.New("invalid groups selector") + } + trimmed = selectorSplit[1] + + accessorSplit := strings.SplitN(trimmed, ".", 2) + if len(accessorSplit) != 2 { + return "", errors.New("invalid groups accessor") + } + var found *Group + for _, group := range groups { + compare := group.Name + if ids { + compare = group.ID + } + + if compare == accessorSplit[0] { + found = group + break + } + } + + if found == nil { + return "", errors.New("group not found") + } + + trimmed = accessorSplit[1] + + switch { + case trimmed == "id": + return found.ID, nil + case trimmed == "name": + if found.Name == "" { + return "", ErrTemplateValueNotFound + } + return found.Name, nil + case strings.HasPrefix(trimmed, "metadata."): + val, ok := found.Metadata[strings.TrimPrefix(trimmed, "metadata.")] + if !ok { + return "", ErrTemplateValueNotFound + } + return val, nil + } + + return "", ErrTemplateValueNotFound + } + + switch { + case strings.HasPrefix(input, "identity.entity."): + if entity == nil { + return "", ErrNoEntityAttachedToToken + } + return performEntityTemplating(strings.TrimPrefix(input, "identity.entity.")) + + case strings.HasPrefix(input, "identity.groups."): + if len(groups) == 0 { + return "", ErrNoGroupsAttachedToToken + } + return performGroupsTemplating(strings.TrimPrefix(input, "identity.groups.")) + } + + return "", ErrTemplateValueNotFound +} diff --git a/helper/identity/templating_test.go b/helper/identity/templating_test.go new file mode 100644 index 0000000000..8407bf8f16 --- /dev/null +++ b/helper/identity/templating_test.go @@ -0,0 +1,194 @@ +package identity + +import ( + "errors" + "testing" +) + +func TestPopulate_Basic(t *testing.T) { + var tests = []struct { + name string + input string + output string + err error + entityName string + metadata map[string]string + aliasAccessor string + aliasID string + aliasName string + nilEntity bool + validityCheckOnly bool + aliasMetadata map[string]string + groupName string + groupMetadata map[string]string + }{ + { + name: "no_templating", + input: "path foobar {", + output: "path foobar {", + }, + { + name: "only_closing", + input: "path foobar}} {", + err: ErrUnbalancedTemplatingCharacter, + }, + { + name: "closing_in_front", + input: "path }} {{foobar}} {", + err: ErrUnbalancedTemplatingCharacter, + }, + { + name: "closing_in_back", + input: "path {{foobar}} }}", + err: ErrUnbalancedTemplatingCharacter, + }, + { + name: "basic", + input: "path /{{identity.entity.id}}/ {", + output: "path /entityID/ {", + }, + { + name: "multiple", + input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", + entityName: "entityName", + metadata: map[string]string{"foo": "bar"}, + output: "path entityName {\n\tval = bar\n}", + }, + { + name: "multiple_bad_name", + input: "path {{identity.entity.name}} {\n\tval = {{identity.entity.metadata.foo}}\n}", + metadata: map[string]string{"foo": "bar"}, + err: ErrTemplateValueNotFound, + }, + { + name: "unbalanced_close", + input: "path {{identity.entity.id}} {\n\tval = {{ent}}ity.metadata.foo}}\n}", + err: ErrUnbalancedTemplatingCharacter, + }, + { + name: "unbalanced_open", + input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", + err: ErrUnbalancedTemplatingCharacter, + }, + { + name: "no_entity_no_directives", + input: "path {{identity.entity.id}} {\n\tval = {{ent{{ity.metadata.foo}}\n}", + err: ErrNoEntityAttachedToToken, + nilEntity: true, + }, + { + name: "no_entity_no_diretives", + input: "path name {\n\tval = foo\n}", + output: "path name {\n\tval = foo\n}", + nilEntity: true, + }, + { + name: "alias_id_name", + input: "path {{ identity.entity.name}} {\n\tval = {{identity.entity.aliases.foomount.id}}\n}", + entityName: "entityName", + aliasAccessor: "foomount", + aliasID: "aliasID", + metadata: map[string]string{"foo": "bar"}, + output: "path entityName {\n\tval = aliasID\n}", + }, + { + name: "alias_id_name_bad_selector", + input: "path foobar {\n\tval = {{identity.entity.aliases.foomount}}\n}", + aliasAccessor: "foomount", + err: errors.New("invalid alias selector"), + }, + { + name: "alias_id_name_bad_accessor", + input: "path \"foobar\" {\n\tval = {{identity.entity.aliases.barmount.id}}\n}", + aliasAccessor: "foomount", + err: errors.New("alias not found"), + }, + { + name: "alias_id_name", + input: "path \"{{identity.entity.name}}\" {\n\tval = {{identity.entity.aliases.foomount.metadata.zip}}\n}", + entityName: "entityName", + aliasAccessor: "foomount", + aliasID: "aliasID", + metadata: map[string]string{"foo": "bar"}, + aliasMetadata: map[string]string{"zip": "zap"}, + output: "path \"entityName\" {\n\tval = zap\n}", + }, + { + name: "group_name", + input: "path \"{{identity.groups.ids.groupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", + entityName: "entityName", + groupName: "groupName", + output: "path \"groupName\" {\n\tval = entityName\n}", + }, + { + name: "group_bad_id", + input: "path \"{{identity.groups.ids.hroupID.name}}\" {\n\tval = {{identity.entity.name}}\n}", + entityName: "entityName", + groupName: "groupName", + err: errors.New("group not found"), + }, + { + name: "group_id", + input: "path \"{{identity.groups.names.groupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", + entityName: "entityName", + groupName: "groupName", + output: "path \"groupID\" {\n\tval = entityName\n}", + }, + { + name: "group_bad_name", + input: "path \"{{identity.groups.names.hroupName.id}}\" {\n\tval = {{identity.entity.name}}\n}", + entityName: "entityName", + groupName: "groupName", + err: errors.New("group not found"), + }, + } + + for _, test := range tests { + var entity *Entity + if !test.nilEntity { + entity = &Entity{ + ID: "entityID", + Name: test.entityName, + Metadata: test.metadata, + } + } + if test.aliasAccessor != "" { + entity.Aliases = []*Alias{ + &Alias{ + MountAccessor: test.aliasAccessor, + ID: test.aliasID, + Name: test.aliasName, + Metadata: test.aliasMetadata, + }, + } + } + var groups []*Group + if test.groupName != "" { + groups = append(groups, &Group{ + ID: "groupID", + Name: test.groupName, + Metadata: test.groupMetadata, + }) + } + subst, out, err := PopulateString(&PopulateStringInput{ + ValidityCheckOnly: test.validityCheckOnly, + String: test.input, + Entity: entity, + Groups: groups, + }) + if err != nil { + if test.err == nil { + t.Fatalf("%s: expected success, got error: %v", test.name, err) + } + if err.Error() != test.err.Error() { + t.Fatalf("%s: got error: %v", test.name, err) + } + } + if out != test.output { + t.Fatalf("%s: bad output: %s", test.name, out) + } + if err == nil && !subst && out != test.input { + t.Fatalf("%s: bad subst flag", test.name) + } + } +} diff --git a/vault/core.go b/vault/core.go index 98018e9ff7..abf9de3e69 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1003,6 +1003,10 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(req) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + c.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor", "error", err) + err = logical.ErrPermissionDenied + } retErr = multierror.Append(retErr, err) c.stateLock.RUnlock() return retErr diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 6e37190e67..52cb6a588f 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -43,7 +43,13 @@ func (d dynamicSystemView) SudoPrivilege(ctx context.Context, path string, token } // Construct the corresponding ACL object - acl, err := d.core.policyStore.ACL(ctx, te.Policies...) + entity, entityPolicies, err := d.core.fetchEntityAndDerivedPolicies(te.EntityID) + if err != nil { + d.core.logger.Error("failed to fetch entity information", "error", err) + return false + } + + acl, err := d.core.policyStore.ACL(ctx, entity, append(entityPolicies, te.Policies...)...) if err != nil { d.core.logger.Error("failed to retrieve ACL for token's policies", "token_policies", te.Policies, "error", err) return false diff --git a/vault/ha.go b/vault/ha.go index 8a76ad1b84..1581095af6 100644 --- a/vault/ha.go +++ b/vault/ha.go @@ -172,6 +172,10 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(req) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + c.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor", "error", err) + err = logical.ErrPermissionDenied + } retErr = multierror.Append(retErr, err) return retErr } diff --git a/vault/logical_system.go b/vault/logical_system.go index 98c620af90..e0023747df 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -2666,6 +2666,7 @@ func (b *SystemBackend) handlePoliciesSet(policyType PolicyType) framework.Opera return handleError(err) } policy.Paths = p.Paths + policy.Templated = p.Templated default: return logical.ErrorResponse("unknown policy type"), nil @@ -3509,6 +3510,10 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic // Load the ACL policies so we can walk the prefix for this mount acl, te, entity, _, err = b.Core.fetchACLTokenEntryAndEntity(req) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + b.Core.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor", "error", err) + err = logical.ErrPermissionDenied + } return nil, err } if entity != nil && entity.Disabled { @@ -3594,6 +3599,10 @@ func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logica // Load the ACL policies so we can walk the prefix for this mount acl, te, entity, _, err := b.Core.fetchACLTokenEntryAndEntity(req) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + b.Core.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor", "error", err) + err = logical.ErrPermissionDenied + } return nil, err } if entity != nil && entity.Disabled { @@ -3620,6 +3629,10 @@ func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *log acl, te, entity, _, err := b.Core.fetchACLTokenEntryAndEntity(req) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + b.Core.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor", "error", err) + err = logical.ErrPermissionDenied + } return nil, err } diff --git a/vault/policy.go b/vault/policy.go index c74d9f3c86..fd10994e13 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/vault/helper/hclutil" + "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/parseutil" "github.com/mitchellh/copystructure" ) @@ -81,10 +82,11 @@ var ( // Policy is used to represent the policy specified by // an ACL configuration. type Policy struct { - Name string `hcl:"name"` - Paths []*PathRules `hcl:"-"` - Raw string - Type PolicyType + Name string `hcl:"name"` + Paths []*PathRules `hcl:"-"` + Raw string + Type PolicyType + Templated bool } // PathRules represents a policy for a path in the namespace. @@ -152,6 +154,15 @@ func (p *ACLPermissions) Clone() (*ACLPermissions, error) { // intermediary set of policies, before being compiled into // the ACL func ParseACLPolicy(rules string) (*Policy, error) { + // Check for templating + hasTemplating, _, err := identity.PopulateString(&identity.PopulateStringInput{ + ValidityCheckOnly: true, + String: rules, + }) + if err != nil { + return nil, errwrap.Wrapf("failed to validate policy templating: {{err}}", err) + } + // Parse the rules root, err := hcl.Parse(rules) if err != nil { @@ -177,6 +188,7 @@ func ParseACLPolicy(rules string) (*Policy, error) { var p Policy p.Raw = rules p.Type = PolicyTypeACL + p.Templated = hasTemplating if err := hcl.DecodeObject(&p, list); err != nil { return nil, errwrap.Wrapf("failed to parse policy: {{err}}", err) } diff --git a/vault/policy_store.go b/vault/policy_store.go index 5e74a92b61..9889dd7b03 100644 --- a/vault/policy_store.go +++ b/vault/policy_store.go @@ -12,6 +12,7 @@ import ( log "github.com/hashicorp/go-hclog" "github.com/hashicorp/golang-lru" "github.com/hashicorp/vault/helper/consts" + "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) @@ -165,9 +166,10 @@ type PolicyStore struct { // PolicyEntry is used to store a policy by name type PolicyEntry struct { - Version int - Raw string - Type PolicyType + Version int + Raw string + Templated bool + Type PolicyType } // NewPolicyStore creates a new PolicyStore that is backed @@ -275,9 +277,10 @@ func (ps *PolicyStore) setPolicyInternal(ctx context.Context, p *Policy) error { defer ps.modifyLock.Unlock() // Create the entry entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{ - Version: 2, - Raw: p.Raw, - Type: p.Type, + Version: 2, + Raw: p.Raw, + Type: p.Type, + Templated: p.Templated, }) if err != nil { return errwrap.Wrapf("failed to create entry: {{err}}", err) @@ -377,6 +380,7 @@ func (ps *PolicyStore) GetPolicy(ctx context.Context, name string, policyType Po policy.Name = name policy.Raw = policyEntry.Raw policy.Type = policyEntry.Type + policy.Templated = policyEntry.Templated switch policyEntry.Type { case PolicyTypeACL: // Parse normally @@ -471,9 +475,21 @@ func (ps *PolicyStore) DeletePolicy(ctx context.Context, name string, policyType return nil } +type TemplateError struct { + Err error +} + +func (t *TemplateError) WrappedErrors() []error { + return []error{t.Err} +} + +func (t *TemplateError) Error() string { + return t.Err.Error() +} + // ACL is used to return an ACL which is built using the // named policies. -func (ps *PolicyStore) ACL(ctx context.Context, names ...string) (*ACL, error) { +func (ps *PolicyStore) ACL(ctx context.Context, entity *identity.Entity, names ...string) (*ACL, error) { // Fetch the policies var policies []*Policy for _, name := range names { @@ -481,7 +497,42 @@ func (ps *PolicyStore) ACL(ctx context.Context, names ...string) (*ACL, error) { if err != nil { return nil, errwrap.Wrapf("failed to get policy: {{err}}", err) } - policies = append(policies, p) + if p != nil { + policies = append(policies, p) + } + } + + var fetchedGroups bool + var groups []*identity.Group + for i, policy := range policies { + if policy.Type == PolicyTypeACL && policy.Templated { + if !fetchedGroups { + fetchedGroups = true + if entity != nil { + directGroups, inheritedGroups, err := ps.core.identityStore.groupsByEntityID(entity.ID) + if err != nil { + return nil, errwrap.Wrapf("failed to fetch group memberships: {{err}}", err) + } + groups = append(directGroups, inheritedGroups...) + } + } + subst, templated, err := identity.PopulateString(&identity.PopulateStringInput{ + String: policy.Raw, + Entity: entity, + Groups: groups, + }) + if err != nil { + return nil, &TemplateError{Err: errwrap.Wrapf(fmt.Sprintf("error performing templating on policy %q: {{err}}", policy.Name), err)} + } + if !subst { + ps.logger.Warn("found templated policy that reported no substitutions", "policy", policy.Name) + } + p, err := ParseACLPolicy(templated) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("error parsing templated policy %q: {{err}}", policy.Name), err) + } + policies[i] = p + } } // Construct the ACL diff --git a/vault/policy_store_ext_test.go b/vault/policy_store_ext_test.go new file mode 100644 index 0000000000..6f7244011a --- /dev/null +++ b/vault/policy_store_ext_test.go @@ -0,0 +1,203 @@ +package vault_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/vault/api" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/vault" +) + +func TestPolicyTemplating(t *testing.T) { + + goodPolicy1 := ` +path "secret/{{ identity.entity.name}}/*" { + capabilities = ["read", "create", "update"] + +} + +path "secret/{{ identity.entity.aliases.%s.name}}/*" { + capabilities = ["read", "create", "update"] + +} +` + + goodPolicy2 := ` +path "secret/{{ identity.groups.ids.%s.name}}/*" { + capabilities = ["read", "create", "update"] + +} + +path "secret/{{ identity.groups.names.%s.id}}/*" { + capabilities = ["read", "create", "update"] + +} +` + + badPolicy1 := ` +path "secret/{{ identity.groups.ids.foobar.name}}/*" { + capabilities = ["read", "create", "update"] + +} +` + + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": credUserpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + resp, err := client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "entity_name", + "policies": []string{ + "goodPolicy1", + "badPolicy1", + }, + }) + if err != nil { + t.Fatal(err) + } + entityID := resp.Data["id"].(string) + + resp, err = client.Logical().Write("identity/group", map[string]interface{}{ + "policies": []string{ + "goodPolicy2", + }, + "member_entity_ids": []string{ + entityID, + }, + "name": "group_name", + }) + if err != nil { + t.Fatal(err) + } + groupID := resp.Data["id"] + + // Enable userpass auth + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + // Create an external group and renew the token. This should add external + // group policies to the token. + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + userpassAccessor := auths["userpass/"].Accessor + + // Create an alias + resp, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser", + "mount_accessor": userpassAccessor, + "canonical_id": entityID, + }) + if err != nil { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + // Add a user to userpass backend + _, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatal(err) + } + + // Write in policies + goodPolicy1 = fmt.Sprintf(goodPolicy1, userpassAccessor) + goodPolicy2 = fmt.Sprintf(goodPolicy2, groupID, "group_name") + err = client.Sys().PutPolicy("goodPolicy1", goodPolicy1) + if err != nil { + t.Fatal(err) + } + err = client.Sys().PutPolicy("goodPolicy2", goodPolicy2) + if err != nil { + t.Fatal(err) + } + + // Authenticate + secret, err := client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatal(err) + } + clientToken := secret.Auth.ClientToken + + var tests = []struct { + name string + path string + fail bool + }{ + { + name: "entity name", + path: "secret/entity_name/foo", + }, + { + name: "bad entity name", + path: "secret/entityname/foo", + fail: true, + }, + { + name: "group name", + path: "secret/group_name/foo", + }, + { + name: "group id", + path: fmt.Sprintf("secret/%s/foo", groupID), + }, + { + name: "alias name", + path: "secret/testuser/foo", + }, + } + + rootToken := client.Token() + client.SetToken(clientToken) + for _, test := range tests { + resp, err := client.Logical().Write(test.path, map[string]interface{}{"zip": "zap"}) + if err != nil && !test.fail { + if resp.Data["error"].(string) != "permission denied" { + t.Fatalf("unexpected status %v", resp.Data["error"]) + } + t.Fatalf("%s: got unexpected error: %v", test.name, err) + } + if err == nil && test.fail { + t.Fatalf("%s: expected error", test.name) + } + } + + client.SetToken(rootToken) + err = client.Sys().PutPolicy("badPolicy1", badPolicy1) + if err != nil { + t.Fatal(err) + } + + client.SetToken(clientToken) + resp, err = client.Logical().Write("secret/entity_name/foo", map[string]interface{}{"zip": "zap"}) + if err == nil { + t.Fatalf("expected error, resp is %#v", *resp) + } + if !strings.Contains(err.Error(), "permission denied") { + t.Fatalf("unexpected status: %v", err) + //if resp.Data["error"].(string) != "permission denied" { + //t.Fatalf("unexpected status %v", resp.Data["error"]) + } +} diff --git a/vault/policy_store_test.go b/vault/policy_store_test.go index 68b96baa21..f54e532421 100644 --- a/vault/policy_store_test.go +++ b/vault/policy_store_test.go @@ -196,7 +196,7 @@ func TestPolicyStore_ACL(t *testing.T) { t.Fatalf("err: %v", err) } - acl, err := ps.ACL(context.Background(), "dev", "ops") + acl, err := ps.ACL(context.Background(), nil, "dev", "ops") if err != nil { t.Fatalf("err: %v", err) } diff --git a/vault/request_handling.go b/vault/request_handling.go index c5adce2d6f..c65ea173b6 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -154,8 +154,11 @@ func (c *Core) fetchACLTokenEntryAndEntity(req *logical.Request) (*ACL, *logical allPolicies := append(te.Policies, identityPolicies...) // Construct the corresponding ACL object - acl, err := c.policyStore.ACL(c.activeContext, allPolicies...) + acl, err := c.policyStore.ACL(c.activeContext, entity, allPolicies...) if err != nil { + if errwrap.ContainsType(err, new(TemplateError)) { + return nil, nil, nil, nil, err + } c.logger.Error("failed to construct ACL", "error", err) return nil, nil, nil, nil, ErrInternalError } @@ -181,6 +184,10 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool // unauth, we just have no information to attach to the request, so // ignore errors...this was best-effort anyways if err != nil && !unauth { + if errwrap.ContainsType(err, new(TemplateError)) { + c.logger.Warn("permission denied due to a templated policy being invalid or containing directives not satisfied by the requestor") + err = logical.ErrPermissionDenied + } return nil, te, err } }