mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
ACL Templating (#4994)
* Initial work on templating * Add check for unbalanced closing in front * Add missing templated assignment * Add first cut of end-to-end test on templating. * Make template errors be 403s and finish up testing * Review feedback
This commit is contained in:
committed by
Brian Kassouf
parent
3f0c33937d
commit
9ccbb91a22
204
helper/identity/templating.go
Normal file
204
helper/identity/templating.go
Normal file
@@ -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
|
||||
}
|
||||
194
helper/identity/templating_test.go
Normal file
194
helper/identity/templating_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
203
vault/policy_store_ext_test.go
Normal file
203
vault/policy_store_ext_test.go
Normal file
@@ -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"])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user