github.com/hashicorp/vault/sdk@v0.11.0/helper/identitytpl/templating.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package identitytpl
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/hashicorp/errwrap"
    15  	"github.com/hashicorp/go-secure-stdlib/parseutil"
    16  	"github.com/hashicorp/vault/sdk/logical"
    17  )
    18  
    19  var (
    20  	ErrUnbalancedTemplatingCharacter = errors.New("unbalanced templating characters")
    21  	ErrNoEntityAttachedToToken       = errors.New("string contains entity template directives but no entity was provided")
    22  	ErrNoGroupsAttachedToToken       = errors.New("string contains groups template directives but no groups were provided")
    23  	ErrTemplateValueNotFound         = errors.New("no value could be found for one of the template directives")
    24  )
    25  
    26  const (
    27  	ACLTemplating = iota // must be the first value for backwards compatibility
    28  	JSONTemplating
    29  )
    30  
    31  type PopulateStringInput struct {
    32  	String            string
    33  	ValidityCheckOnly bool
    34  	Entity            *logical.Entity
    35  	Groups            []*logical.Group
    36  	NamespaceID       string
    37  	Mode              int       // processing mode, ACLTemplate or JSONTemplating
    38  	Now               time.Time // optional, defaults to current time
    39  
    40  	templateHandler templateHandlerFunc
    41  	groupIDs        []string
    42  	groupNames      []string
    43  }
    44  
    45  // templateHandlerFunc allows generating string outputs based on data type, and
    46  // different handlers can be used based on mode. For example in ACL mode, strings
    47  // are emitted verbatim, but they're wrapped in double quotes for JSON mode. And
    48  // some structures, like slices, might be rendered in one mode but prohibited in
    49  // another.
    50  type templateHandlerFunc func(interface{}, ...string) (string, error)
    51  
    52  // aclTemplateHandler processes known parameter data types when operating
    53  // in ACL mode.
    54  func aclTemplateHandler(v interface{}, keys ...string) (string, error) {
    55  	switch t := v.(type) {
    56  	case string:
    57  		if t == "" {
    58  			return "", ErrTemplateValueNotFound
    59  		}
    60  		return t, nil
    61  	case []string:
    62  		return "", ErrTemplateValueNotFound
    63  	case map[string]string:
    64  		if len(keys) > 0 {
    65  			val, ok := t[keys[0]]
    66  			if ok {
    67  				return val, nil
    68  			}
    69  		}
    70  		return "", ErrTemplateValueNotFound
    71  	}
    72  
    73  	return "", fmt.Errorf("unknown type: %T", v)
    74  }
    75  
    76  // jsonTemplateHandler processes known parameter data types when operating
    77  // in JSON mode.
    78  func jsonTemplateHandler(v interface{}, keys ...string) (string, error) {
    79  	jsonMarshaller := func(v interface{}) (string, error) {
    80  		enc, err := json.Marshal(v)
    81  		if err != nil {
    82  			return "", err
    83  		}
    84  		return string(enc), nil
    85  	}
    86  
    87  	switch t := v.(type) {
    88  	case string:
    89  		return strconv.Quote(t), nil
    90  	case []string:
    91  		return jsonMarshaller(t)
    92  	case map[string]string:
    93  		if len(keys) > 0 {
    94  			return strconv.Quote(t[keys[0]]), nil
    95  		}
    96  		if t == nil {
    97  			return "{}", nil
    98  		}
    99  		return jsonMarshaller(t)
   100  	}
   101  
   102  	return "", fmt.Errorf("unknown type: %T", v)
   103  }
   104  
   105  func PopulateString(p PopulateStringInput) (bool, string, error) {
   106  	if p.String == "" {
   107  		return false, "", nil
   108  	}
   109  
   110  	// preprocess groups
   111  	for _, g := range p.Groups {
   112  		p.groupNames = append(p.groupNames, g.Name)
   113  		p.groupIDs = append(p.groupIDs, g.ID)
   114  	}
   115  
   116  	// set up mode-specific handler
   117  	switch p.Mode {
   118  	case ACLTemplating:
   119  		p.templateHandler = aclTemplateHandler
   120  	case JSONTemplating:
   121  		p.templateHandler = jsonTemplateHandler
   122  	default:
   123  		return false, "", fmt.Errorf("unknown mode %q", p.Mode)
   124  	}
   125  
   126  	var subst bool
   127  	splitStr := strings.Split(p.String, "{{")
   128  
   129  	if len(splitStr) >= 1 {
   130  		if strings.Contains(splitStr[0], "}}") {
   131  			return false, "", ErrUnbalancedTemplatingCharacter
   132  		}
   133  		if len(splitStr) == 1 {
   134  			return false, p.String, nil
   135  		}
   136  	}
   137  
   138  	var b strings.Builder
   139  	if !p.ValidityCheckOnly {
   140  		b.Grow(2 * len(p.String))
   141  	}
   142  
   143  	for i, str := range splitStr {
   144  		if i == 0 {
   145  			if !p.ValidityCheckOnly {
   146  				b.WriteString(str)
   147  			}
   148  			continue
   149  		}
   150  		splitPiece := strings.Split(str, "}}")
   151  		switch len(splitPiece) {
   152  		case 2:
   153  			subst = true
   154  			if !p.ValidityCheckOnly {
   155  				tmplStr, err := performTemplating(strings.TrimSpace(splitPiece[0]), &p)
   156  				if err != nil {
   157  					return false, "", err
   158  				}
   159  				b.WriteString(tmplStr)
   160  				b.WriteString(splitPiece[1])
   161  			}
   162  		default:
   163  			return false, "", ErrUnbalancedTemplatingCharacter
   164  		}
   165  	}
   166  
   167  	return subst, b.String(), nil
   168  }
   169  
   170  func performTemplating(input string, p *PopulateStringInput) (string, error) {
   171  	performAliasTemplating := func(trimmed string, alias *logical.Alias) (string, error) {
   172  		switch {
   173  		case trimmed == "id":
   174  			return p.templateHandler(alias.ID)
   175  
   176  		case trimmed == "name":
   177  			return p.templateHandler(alias.Name)
   178  
   179  		case trimmed == "metadata":
   180  			return p.templateHandler(alias.Metadata)
   181  
   182  		case strings.HasPrefix(trimmed, "metadata."):
   183  			split := strings.SplitN(trimmed, ".", 2)
   184  			return p.templateHandler(alias.Metadata, split[1])
   185  
   186  		case trimmed == "custom_metadata":
   187  			return p.templateHandler(alias.CustomMetadata)
   188  
   189  		case strings.HasPrefix(trimmed, "custom_metadata."):
   190  
   191  			split := strings.SplitN(trimmed, ".", 2)
   192  			return p.templateHandler(alias.CustomMetadata, split[1])
   193  
   194  		}
   195  
   196  		return "", ErrTemplateValueNotFound
   197  	}
   198  
   199  	performEntityTemplating := func(trimmed string) (string, error) {
   200  		switch {
   201  		case trimmed == "id":
   202  			return p.templateHandler(p.Entity.ID)
   203  
   204  		case trimmed == "name":
   205  			return p.templateHandler(p.Entity.Name)
   206  
   207  		case trimmed == "metadata":
   208  			return p.templateHandler(p.Entity.Metadata)
   209  
   210  		case strings.HasPrefix(trimmed, "metadata."):
   211  			split := strings.SplitN(trimmed, ".", 2)
   212  			return p.templateHandler(p.Entity.Metadata, split[1])
   213  
   214  		case trimmed == "groups.names":
   215  			return p.templateHandler(p.groupNames)
   216  
   217  		case trimmed == "groups.ids":
   218  			return p.templateHandler(p.groupIDs)
   219  
   220  		case strings.HasPrefix(trimmed, "aliases."):
   221  			split := strings.SplitN(strings.TrimPrefix(trimmed, "aliases."), ".", 2)
   222  			if len(split) != 2 {
   223  				return "", errors.New("invalid alias selector")
   224  			}
   225  			var alias *logical.Alias
   226  			for _, a := range p.Entity.Aliases {
   227  				if split[0] == a.MountAccessor {
   228  					alias = a
   229  					break
   230  				}
   231  			}
   232  			if alias == nil {
   233  				if p.Mode == ACLTemplating {
   234  					return "", errors.New("alias not found")
   235  				}
   236  
   237  				// An empty alias is sufficient for generating defaults
   238  				alias = &logical.Alias{Metadata: make(map[string]string), CustomMetadata: make(map[string]string)}
   239  			}
   240  			return performAliasTemplating(split[1], alias)
   241  		}
   242  
   243  		return "", ErrTemplateValueNotFound
   244  	}
   245  
   246  	performGroupsTemplating := func(trimmed string) (string, error) {
   247  		var ids bool
   248  
   249  		selectorSplit := strings.SplitN(trimmed, ".", 2)
   250  
   251  		switch {
   252  		case len(selectorSplit) != 2:
   253  			return "", errors.New("invalid groups selector")
   254  
   255  		case selectorSplit[0] == "ids":
   256  			ids = true
   257  
   258  		case selectorSplit[0] == "names":
   259  
   260  		default:
   261  			return "", errors.New("invalid groups selector")
   262  		}
   263  		trimmed = selectorSplit[1]
   264  
   265  		accessorSplit := strings.SplitN(trimmed, ".", 2)
   266  		if len(accessorSplit) != 2 {
   267  			return "", errors.New("invalid groups accessor")
   268  		}
   269  		var found *logical.Group
   270  		for _, group := range p.Groups {
   271  			var compare string
   272  			if ids {
   273  				compare = group.ID
   274  			} else {
   275  				if p.NamespaceID != "" && group.NamespaceID != p.NamespaceID {
   276  					continue
   277  				}
   278  				compare = group.Name
   279  			}
   280  
   281  			if compare == accessorSplit[0] {
   282  				found = group
   283  				break
   284  			}
   285  		}
   286  
   287  		if found == nil {
   288  			return "", fmt.Errorf("entity is not a member of group %q", accessorSplit[0])
   289  		}
   290  
   291  		trimmed = accessorSplit[1]
   292  
   293  		switch {
   294  		case trimmed == "id":
   295  			return found.ID, nil
   296  
   297  		case trimmed == "name":
   298  			if found.Name == "" {
   299  				return "", ErrTemplateValueNotFound
   300  			}
   301  			return found.Name, nil
   302  
   303  		case strings.HasPrefix(trimmed, "metadata."):
   304  			val, ok := found.Metadata[strings.TrimPrefix(trimmed, "metadata.")]
   305  			if !ok {
   306  				return "", ErrTemplateValueNotFound
   307  			}
   308  			return val, nil
   309  		}
   310  
   311  		return "", ErrTemplateValueNotFound
   312  	}
   313  
   314  	performTimeTemplating := func(trimmed string) (string, error) {
   315  		now := p.Now
   316  		if now.IsZero() {
   317  			now = time.Now()
   318  		}
   319  
   320  		opsSplit := strings.SplitN(trimmed, ".", 3)
   321  
   322  		if opsSplit[0] != "now" {
   323  			return "", fmt.Errorf("invalid time selector %q", opsSplit[0])
   324  		}
   325  
   326  		result := now
   327  		switch len(opsSplit) {
   328  		case 1:
   329  			// return current time
   330  		case 2:
   331  			return "", errors.New("missing time operand")
   332  
   333  		case 3:
   334  			duration, err := parseutil.ParseDurationSecond(opsSplit[2])
   335  			if err != nil {
   336  				return "", errwrap.Wrapf("invalid duration: {{err}}", err)
   337  			}
   338  
   339  			switch opsSplit[1] {
   340  			case "plus":
   341  				result = result.Add(duration)
   342  			case "minus":
   343  				result = result.Add(-duration)
   344  			default:
   345  				return "", fmt.Errorf("invalid time operator %q", opsSplit[1])
   346  			}
   347  		}
   348  
   349  		return strconv.FormatInt(result.Unix(), 10), nil
   350  	}
   351  
   352  	switch {
   353  	case strings.HasPrefix(input, "identity.entity."):
   354  		if p.Entity == nil {
   355  			return "", ErrNoEntityAttachedToToken
   356  		}
   357  		return performEntityTemplating(strings.TrimPrefix(input, "identity.entity."))
   358  
   359  	case strings.HasPrefix(input, "identity.groups."):
   360  		if len(p.Groups) == 0 {
   361  			return "", ErrNoGroupsAttachedToToken
   362  		}
   363  		return performGroupsTemplating(strings.TrimPrefix(input, "identity.groups."))
   364  
   365  	case strings.HasPrefix(input, "time."):
   366  		return performTimeTemplating(strings.TrimPrefix(input, "time."))
   367  	}
   368  
   369  	return "", ErrTemplateValueNotFound
   370  }