Skip to content

Commit

Permalink
Support token's metadata in template
Browse files Browse the repository at this point in the history
  • Loading branch information
UXabre authored and Arend Lapere committed Oct 18, 2021
1 parent 8d438a9 commit e9c3ed0
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 11 deletions.
3 changes: 3 additions & 0 deletions changelog/10682.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
core: support token metadata in template
```
21 changes: 21 additions & 0 deletions sdk/helper/identitytpl/templating.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

var (
ErrUnbalancedTemplatingCharacter = errors.New("unbalanced templating characters")
ErrNoAttachedToken = errors.New("string contains entity template directives but no token was provided")
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")
Expand All @@ -27,6 +28,7 @@ const (
type PopulateStringInput struct {
String string
ValidityCheckOnly bool
Token *logical.TokenEntry
Entity *logical.Entity
Groups []*logical.Group
NamespaceID string
Expand Down Expand Up @@ -192,6 +194,18 @@ func performTemplating(input string, p *PopulateStringInput) (string, error) {
return "", ErrTemplateValueNotFound
}

performTokenTemplating := func(trimmed string) (string, error) {
switch {
case trimmed == "metadata":
return p.templateHandler(p.Token.Meta)
case strings.HasPrefix(trimmed, "metadata."):
split := strings.SplitN(trimmed, ".", 2)
return p.templateHandler(p.Token.Meta, split[1])
}

return "", ErrTemplateValueNotFound
}

performEntityTemplating := func(trimmed string) (string, error) {
switch {
case trimmed == "id":
Expand Down Expand Up @@ -352,6 +366,13 @@ func performTemplating(input string, p *PopulateStringInput) (string, error) {
}
return performEntityTemplating(strings.TrimPrefix(input, "identity.entity."))

case strings.HasPrefix(input, "token."):
if p.Token == nil {
return "", ErrNoAttachedToken
}

return performTokenTemplating(strings.TrimPrefix(input, "token."))

case strings.HasPrefix(input, "identity.groups."):
if len(p.Groups) == 0 {
return "", ErrNoGroupsAttachedToToken
Expand Down
38 changes: 38 additions & 0 deletions sdk/helper/identitytpl/templating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,41 @@ func TestPopulate_FullObject(t *testing.T) {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out)
}
}

func TestPopulate_TokenMetaData(t *testing.T) {
testToken := &logical.TokenEntry{
Meta: map[string]string{
"color": "green",
"size": "small",
"non-printable": "\"\n\t",
},
}

template := `
{
"all metadata": {{token.metadata}},
"one metadata key": {{token.metadata.color}},
"one metadata key not found": {{token.metadata.asldfk}}
}`

expected := `
{
"all metadata": {"color":"green","non-printable":"\"\n\t","size":"small"},
"one metadata key": "green",
"one metadata key not found": ""
}`

input := PopulateStringInput{
Mode: JSONTemplating,
String: template,
Token: testToken,
}
_, out, err := PopulateString(input)
if err != nil {
t.Fatal(err)
}

if out != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, out)
}
}
4 changes: 2 additions & 2 deletions vault/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string,
return nil, &logical.StatusBadRequest{Err: "missing token"}
}

te, err := c.tokenStore.Lookup(ctx, token)
te, err := c.LookupToken(ctx, token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -76,7 +76,7 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string,
// Construct the corresponding ACL object. ACL construction should be
// performed on the token's namespace.
tokenCtx := namespace.ContextWithNamespace(ctx, tokenNS)
acl, err := c.policyStore.ACL(tokenCtx, entity, policyNames, policies...)
acl, err := c.policyStore.ACL(tokenCtx, entity, te, policyNames, policies...)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion vault/dynamic_system_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (e extendedSystemViewImpl) SudoPrivilege(ctx context.Context, path string,

// Construct the corresponding ACL object. Derive and use a new context that
// uses the req.ClientToken's namespace
acl, err := e.core.policyStore.ACL(tokenCtx, entity, policyNames, policies...)
acl, err := e.core.policyStore.ACL(tokenCtx, entity, te, policyNames, policies...)
if err != nil {
e.core.logger.Error("failed to retrieve ACL for token's policies", "token_policies", te.Policies, "error", err)
return false
Expand Down
10 changes: 6 additions & 4 deletions vault/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/hclutil"
"github.com/hashicorp/vault/sdk/helper/identitytpl"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
)

Expand Down Expand Up @@ -232,14 +233,14 @@ func (p *ACLPermissions) Clone() (*ACLPermissions, error) {
// intermediary set of policies, before being compiled into
// the ACL
func ParseACLPolicy(ns *namespace.Namespace, rules string) (*Policy, error) {
return parseACLPolicyWithTemplating(ns, rules, false, nil, nil)
return parseACLPolicyWithTemplating(ns, rules, false, nil, nil, nil)
}

// parseACLPolicyWithTemplating performs the actual work and checks whether we
// should perform substitutions. If performTemplating is true we know that it
// is templated so we don't check again, otherwise we check to see if it's a
// templated policy.
func parseACLPolicyWithTemplating(ns *namespace.Namespace, rules string, performTemplating bool, entity *identity.Entity, groups []*identity.Group) (*Policy, error) {
func parseACLPolicyWithTemplating(ns *namespace.Namespace, rules string, performTemplating bool, entity *identity.Entity, groups []*identity.Group, token *logical.TokenEntry) (*Policy, error) {
// Parse the rules
root, err := hcl.Parse(rules)
if err != nil {
Expand Down Expand Up @@ -272,15 +273,15 @@ func parseACLPolicyWithTemplating(ns *namespace.Namespace, rules string, perform
}

if o := list.Filter("path"); len(o.Items) > 0 {
if err := parsePaths(&p, o, performTemplating, entity, groups); err != nil {
if err := parsePaths(&p, o, performTemplating, entity, groups, token); err != nil {
return nil, fmt.Errorf("failed to parse policy: %w", err)
}
}

return &p, nil
}

func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, entity *identity.Entity, groups []*identity.Group) error {
func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, entity *identity.Entity, groups []*identity.Group, token *logical.TokenEntry) error {
paths := make([]*PathRules, 0, len(list.Items))
for _, item := range list.Items {
key := "path"
Expand All @@ -293,6 +294,7 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
_, templated, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
Mode: identitytpl.ACLTemplating,
String: key,
Token: token,
Entity: identity.ToSDKEntity(entity),
Groups: identity.ToSDKGroups(groups),
NamespaceID: result.namespace.ID,
Expand Down
4 changes: 2 additions & 2 deletions vault/policy_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ func (t *TemplateError) Error() string {

// ACL is used to return an ACL which is built using the
// named policies and pre-fetched policies if given.
func (ps *PolicyStore) ACL(ctx context.Context, entity *identity.Entity, policyNames map[string][]string, additionalPolicies ...*Policy) (*ACL, error) {
func (ps *PolicyStore) ACL(ctx context.Context, entity *identity.Entity, token *logical.TokenEntry, policyNames map[string][]string, additionalPolicies ...*Policy) (*ACL, error) {
var allPolicies []*Policy

// Fetch the named policies
Expand Down Expand Up @@ -795,7 +795,7 @@ func (ps *PolicyStore) ACL(ctx context.Context, entity *identity.Entity, policyN
groups = append(directGroups, inheritedGroups...)
}
}
p, err := parseACLPolicyWithTemplating(policy.namespace, policy.Raw, true, entity, groups)
p, err := parseACLPolicyWithTemplating(policy.namespace, policy.Raw, true, entity, groups, token)
if err != nil {
return nil, fmt.Errorf("error parsing templated policy %q: %w", policy.Name, err)
}
Expand Down
2 changes: 1 addition & 1 deletion vault/policy_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func testPolicyStoreACL(t *testing.T, ps *PolicyStore, ns *namespace.Namespace)
}

ctx = namespace.ContextWithNamespace(context.Background(), ns)
acl, err := ps.ACL(ctx, nil, map[string][]string{ns.ID: {"dev", "ops"}})
acl, err := ps.ACL(ctx, nil, nil, map[string][]string{ns.ID: {"dev", "ops"}})
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion vault/request_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req

// Construct the corresponding ACL object. ACL construction should be
// performed on the token's namespace.
acl, err := c.policyStore.ACL(tokenCtx, entity, policyNames, policies...)
acl, err := c.policyStore.ACL(tokenCtx, entity, te, policyNames, policies...)
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)
Expand Down
1 change: 1 addition & 0 deletions website/content/docs/concepts/policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ injected, and currently the `path` keys in policies allow injection.

| Name | Description |
| :----------------------------------------------------------------- | :---------------------------------------------------------------------- |
| `token.metadata.<metadata key>` | The token's associated metadata |
| `identity.entity.id` | The entity's ID |
| `identity.entity.name` | The entity's name |
| `identity.entity.metadata.<metadata key>` | Metadata associated with the entity for the given key |
Expand Down

0 comments on commit e9c3ed0

Please sign in to comment.