359 lines
8.6 KiB
Go
359 lines
8.6 KiB
Go
package identitytpl
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
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")
|
|
)
|
|
|
|
const (
|
|
ACLTemplating = iota // must be the first value for backwards compatibility
|
|
JSONTemplating
|
|
)
|
|
|
|
type PopulateStringInput struct {
|
|
String string
|
|
ValidityCheckOnly bool
|
|
Entity *logical.Entity
|
|
Groups []*logical.Group
|
|
NamespaceID string
|
|
Mode int // processing mode, ACLTemplate or JSONTemplating
|
|
Now time.Time // optional, defaults to current time
|
|
|
|
templateHandler templateHandlerFunc
|
|
groupIDs []string
|
|
groupNames []string
|
|
}
|
|
|
|
// templateHandlerFunc allows generating string outputs based on data type, and
|
|
// different handlers can be used based on mode. For example in ACL mode, strings
|
|
// are emitted verbatim, but they're wrapped in double quotes for JSON mode. And
|
|
// some structures, like slices, might be rendered in one mode but prohibited in
|
|
// another.
|
|
type templateHandlerFunc func(interface{}, ...string) (string, error)
|
|
|
|
// aclTemplateHandler processes known parameter data types when operating
|
|
// in ACL mode.
|
|
func aclTemplateHandler(v interface{}, keys ...string) (string, error) {
|
|
switch t := v.(type) {
|
|
case string:
|
|
if t == "" {
|
|
return "", ErrTemplateValueNotFound
|
|
}
|
|
return t, nil
|
|
case []string:
|
|
return "", ErrTemplateValueNotFound
|
|
case map[string]string:
|
|
if len(keys) > 0 {
|
|
val, ok := t[keys[0]]
|
|
if ok {
|
|
return val, nil
|
|
}
|
|
}
|
|
return "", ErrTemplateValueNotFound
|
|
}
|
|
|
|
return "", fmt.Errorf("unknown type: %T", v)
|
|
}
|
|
|
|
// jsonTemplateHandler processes known parameter data types when operating
|
|
// in JSON mode.
|
|
func jsonTemplateHandler(v interface{}, keys ...string) (string, error) {
|
|
jsonMarshaller := func(v interface{}) (string, error) {
|
|
enc, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(enc), nil
|
|
}
|
|
|
|
switch t := v.(type) {
|
|
case string:
|
|
return strconv.Quote(t), nil
|
|
case []string:
|
|
return jsonMarshaller(t)
|
|
case map[string]string:
|
|
if len(keys) > 0 {
|
|
return strconv.Quote(t[keys[0]]), nil
|
|
}
|
|
if t == nil {
|
|
return "{}", nil
|
|
}
|
|
return jsonMarshaller(t)
|
|
}
|
|
|
|
return "", fmt.Errorf("unknown type: %T", v)
|
|
}
|
|
|
|
func PopulateString(p PopulateStringInput) (bool, string, error) {
|
|
if p.String == "" {
|
|
return false, "", nil
|
|
}
|
|
|
|
// preprocess groups
|
|
for _, g := range p.Groups {
|
|
p.groupNames = append(p.groupNames, g.Name)
|
|
p.groupIDs = append(p.groupIDs, g.ID)
|
|
}
|
|
|
|
// set up mode-specific handler
|
|
switch p.Mode {
|
|
case ACLTemplating:
|
|
p.templateHandler = aclTemplateHandler
|
|
case JSONTemplating:
|
|
p.templateHandler = jsonTemplateHandler
|
|
default:
|
|
return false, "", fmt.Errorf("unknown mode %q", p.Mode)
|
|
}
|
|
|
|
var subst bool
|
|
splitStr := strings.Split(p.String, "{{")
|
|
|
|
if len(splitStr) >= 1 {
|
|
if strings.Contains(splitStr[0], "}}") {
|
|
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)
|
|
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, p *PopulateStringInput) (string, error) {
|
|
|
|
performAliasTemplating := func(trimmed string, alias *logical.Alias) (string, error) {
|
|
switch {
|
|
case trimmed == "id":
|
|
return p.templateHandler(alias.ID)
|
|
|
|
case trimmed == "name":
|
|
return p.templateHandler(alias.Name)
|
|
|
|
case trimmed == "metadata":
|
|
return p.templateHandler(alias.Metadata)
|
|
|
|
case strings.HasPrefix(trimmed, "metadata."):
|
|
split := strings.SplitN(trimmed, ".", 2)
|
|
return p.templateHandler(alias.Metadata, split[1])
|
|
}
|
|
|
|
return "", ErrTemplateValueNotFound
|
|
}
|
|
|
|
performEntityTemplating := func(trimmed string) (string, error) {
|
|
switch {
|
|
case trimmed == "id":
|
|
return p.templateHandler(p.Entity.ID)
|
|
|
|
case trimmed == "name":
|
|
return p.templateHandler(p.Entity.Name)
|
|
|
|
case trimmed == "metadata":
|
|
return p.templateHandler(p.Entity.Metadata)
|
|
|
|
case strings.HasPrefix(trimmed, "metadata."):
|
|
split := strings.SplitN(trimmed, ".", 2)
|
|
return p.templateHandler(p.Entity.Metadata, split[1])
|
|
|
|
case trimmed == "groups.names":
|
|
return p.templateHandler(p.groupNames)
|
|
|
|
case trimmed == "groups.ids":
|
|
return p.templateHandler(p.groupIDs)
|
|
|
|
case strings.HasPrefix(trimmed, "aliases."):
|
|
split := strings.SplitN(strings.TrimPrefix(trimmed, "aliases."), ".", 2)
|
|
if len(split) != 2 {
|
|
return "", errors.New("invalid alias selector")
|
|
}
|
|
var alias *logical.Alias
|
|
for _, a := range p.Entity.Aliases {
|
|
if split[0] == a.MountAccessor {
|
|
alias = a
|
|
break
|
|
}
|
|
}
|
|
if alias == nil {
|
|
if p.Mode == ACLTemplating {
|
|
return "", errors.New("alias not found")
|
|
}
|
|
|
|
// An empty alias is sufficient for generating defaults
|
|
alias = &logical.Alias{Metadata: make(map[string]string)}
|
|
}
|
|
return performAliasTemplating(split[1], alias)
|
|
}
|
|
|
|
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 *logical.Group
|
|
for _, group := range p.Groups {
|
|
var compare string
|
|
if ids {
|
|
compare = group.ID
|
|
} else {
|
|
if p.NamespaceID != "" && group.NamespaceID != p.NamespaceID {
|
|
continue
|
|
}
|
|
compare = group.Name
|
|
}
|
|
|
|
if compare == accessorSplit[0] {
|
|
found = group
|
|
break
|
|
}
|
|
}
|
|
|
|
if found == nil {
|
|
return "", fmt.Errorf("entity is not a member of group %q", accessorSplit[0])
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
performTimeTemplating := func(trimmed string) (string, error) {
|
|
now := p.Now
|
|
if now.IsZero() {
|
|
now = time.Now()
|
|
}
|
|
|
|
opsSplit := strings.SplitN(trimmed, ".", 3)
|
|
|
|
if opsSplit[0] != "now" {
|
|
return "", fmt.Errorf("invalid time selector %q", opsSplit[0])
|
|
}
|
|
|
|
result := now
|
|
switch len(opsSplit) {
|
|
case 1:
|
|
// return current time
|
|
case 2:
|
|
return "", errors.New("missing time operand")
|
|
|
|
case 3:
|
|
duration, err := time.ParseDuration(opsSplit[2])
|
|
if err != nil {
|
|
return "", errwrap.Wrapf("invalid duration: {{err}}", err)
|
|
}
|
|
|
|
switch opsSplit[1] {
|
|
case "plus":
|
|
result = result.Add(duration)
|
|
case "minus":
|
|
result = result.Add(-duration)
|
|
default:
|
|
return "", fmt.Errorf("invalid time operator %q", opsSplit[1])
|
|
}
|
|
}
|
|
|
|
return strconv.FormatInt(result.Unix(), 10), nil
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(input, "identity.entity."):
|
|
if p.Entity == nil {
|
|
return "", ErrNoEntityAttachedToToken
|
|
}
|
|
return performEntityTemplating(strings.TrimPrefix(input, "identity.entity."))
|
|
|
|
case strings.HasPrefix(input, "identity.groups."):
|
|
if len(p.Groups) == 0 {
|
|
return "", ErrNoGroupsAttachedToToken
|
|
}
|
|
return performGroupsTemplating(strings.TrimPrefix(input, "identity.groups."))
|
|
|
|
case strings.HasPrefix(input, "time."):
|
|
return performTimeTemplating(strings.TrimPrefix(input, "time."))
|
|
}
|
|
|
|
return "", ErrTemplateValueNotFound
|
|
}
|