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 }