mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +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)
|
acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(req)
|
||||||
if err != nil {
|
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)
|
retErr = multierror.Append(retErr, err)
|
||||||
c.stateLock.RUnlock()
|
c.stateLock.RUnlock()
|
||||||
return retErr
|
return retErr
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ func (d dynamicSystemView) SudoPrivilege(ctx context.Context, path string, token
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct the corresponding ACL object
|
// 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 {
|
if err != nil {
|
||||||
d.core.logger.Error("failed to retrieve ACL for token's policies", "token_policies", te.Policies, "error", err)
|
d.core.logger.Error("failed to retrieve ACL for token's policies", "token_policies", te.Policies, "error", err)
|
||||||
return false
|
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)
|
acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(req)
|
||||||
if err != nil {
|
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)
|
retErr = multierror.Append(retErr, err)
|
||||||
return retErr
|
return retErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2666,6 +2666,7 @@ func (b *SystemBackend) handlePoliciesSet(policyType PolicyType) framework.Opera
|
|||||||
return handleError(err)
|
return handleError(err)
|
||||||
}
|
}
|
||||||
policy.Paths = p.Paths
|
policy.Paths = p.Paths
|
||||||
|
policy.Templated = p.Templated
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return logical.ErrorResponse("unknown policy type"), nil
|
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
|
// Load the ACL policies so we can walk the prefix for this mount
|
||||||
acl, te, entity, _, err = b.Core.fetchACLTokenEntryAndEntity(req)
|
acl, te, entity, _, err = b.Core.fetchACLTokenEntryAndEntity(req)
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if entity != nil && entity.Disabled {
|
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
|
// Load the ACL policies so we can walk the prefix for this mount
|
||||||
acl, te, entity, _, err := b.Core.fetchACLTokenEntryAndEntity(req)
|
acl, te, entity, _, err := b.Core.fetchACLTokenEntryAndEntity(req)
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if entity != nil && entity.Disabled {
|
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)
|
acl, te, entity, _, err := b.Core.fetchACLTokenEntryAndEntity(req)
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/hashicorp/hcl/hcl/ast"
|
"github.com/hashicorp/hcl/hcl/ast"
|
||||||
"github.com/hashicorp/vault/helper/hclutil"
|
"github.com/hashicorp/vault/helper/hclutil"
|
||||||
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/parseutil"
|
"github.com/hashicorp/vault/helper/parseutil"
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
)
|
)
|
||||||
@@ -81,10 +82,11 @@ var (
|
|||||||
// Policy is used to represent the policy specified by
|
// Policy is used to represent the policy specified by
|
||||||
// an ACL configuration.
|
// an ACL configuration.
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
Name string `hcl:"name"`
|
Name string `hcl:"name"`
|
||||||
Paths []*PathRules `hcl:"-"`
|
Paths []*PathRules `hcl:"-"`
|
||||||
Raw string
|
Raw string
|
||||||
Type PolicyType
|
Type PolicyType
|
||||||
|
Templated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathRules represents a policy for a path in the namespace.
|
// 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
|
// intermediary set of policies, before being compiled into
|
||||||
// the ACL
|
// the ACL
|
||||||
func ParseACLPolicy(rules string) (*Policy, error) {
|
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
|
// Parse the rules
|
||||||
root, err := hcl.Parse(rules)
|
root, err := hcl.Parse(rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -177,6 +188,7 @@ func ParseACLPolicy(rules string) (*Policy, error) {
|
|||||||
var p Policy
|
var p Policy
|
||||||
p.Raw = rules
|
p.Raw = rules
|
||||||
p.Type = PolicyTypeACL
|
p.Type = PolicyTypeACL
|
||||||
|
p.Templated = hasTemplating
|
||||||
if err := hcl.DecodeObject(&p, list); err != nil {
|
if err := hcl.DecodeObject(&p, list); err != nil {
|
||||||
return nil, errwrap.Wrapf("failed to parse policy: {{err}}", err)
|
return nil, errwrap.Wrapf("failed to parse policy: {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
log "github.com/hashicorp/go-hclog"
|
log "github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/golang-lru"
|
"github.com/hashicorp/golang-lru"
|
||||||
"github.com/hashicorp/vault/helper/consts"
|
"github.com/hashicorp/vault/helper/consts"
|
||||||
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/strutil"
|
"github.com/hashicorp/vault/helper/strutil"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
)
|
)
|
||||||
@@ -165,9 +166,10 @@ type PolicyStore struct {
|
|||||||
|
|
||||||
// PolicyEntry is used to store a policy by name
|
// PolicyEntry is used to store a policy by name
|
||||||
type PolicyEntry struct {
|
type PolicyEntry struct {
|
||||||
Version int
|
Version int
|
||||||
Raw string
|
Raw string
|
||||||
Type PolicyType
|
Templated bool
|
||||||
|
Type PolicyType
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPolicyStore creates a new PolicyStore that is backed
|
// 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()
|
defer ps.modifyLock.Unlock()
|
||||||
// Create the entry
|
// Create the entry
|
||||||
entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{
|
entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
Raw: p.Raw,
|
Raw: p.Raw,
|
||||||
Type: p.Type,
|
Type: p.Type,
|
||||||
|
Templated: p.Templated,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrapf("failed to create entry: {{err}}", err)
|
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.Name = name
|
||||||
policy.Raw = policyEntry.Raw
|
policy.Raw = policyEntry.Raw
|
||||||
policy.Type = policyEntry.Type
|
policy.Type = policyEntry.Type
|
||||||
|
policy.Templated = policyEntry.Templated
|
||||||
switch policyEntry.Type {
|
switch policyEntry.Type {
|
||||||
case PolicyTypeACL:
|
case PolicyTypeACL:
|
||||||
// Parse normally
|
// Parse normally
|
||||||
@@ -471,9 +475,21 @@ func (ps *PolicyStore) DeletePolicy(ctx context.Context, name string, policyType
|
|||||||
return nil
|
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
|
// ACL is used to return an ACL which is built using the
|
||||||
// named policies.
|
// 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
|
// Fetch the policies
|
||||||
var policies []*Policy
|
var policies []*Policy
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@@ -481,7 +497,42 @@ func (ps *PolicyStore) ACL(ctx context.Context, names ...string) (*ACL, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrapf("failed to get policy: {{err}}", err)
|
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
|
// 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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,8 +154,11 @@ func (c *Core) fetchACLTokenEntryAndEntity(req *logical.Request) (*ACL, *logical
|
|||||||
allPolicies := append(te.Policies, identityPolicies...)
|
allPolicies := append(te.Policies, identityPolicies...)
|
||||||
|
|
||||||
// Construct the corresponding ACL object
|
// 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 err != nil {
|
||||||
|
if errwrap.ContainsType(err, new(TemplateError)) {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
c.logger.Error("failed to construct ACL", "error", err)
|
c.logger.Error("failed to construct ACL", "error", err)
|
||||||
return nil, nil, nil, nil, ErrInternalError
|
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
|
// unauth, we just have no information to attach to the request, so
|
||||||
// ignore errors...this was best-effort anyways
|
// ignore errors...this was best-effort anyways
|
||||||
if err != nil && !unauth {
|
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
|
return nil, te, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user