acl: refactor the authmethod.Validator interface (#7760)

This is a collection of refactors that make upcoming PRs easier to digest.

The main change is the introduction of the authmethod.Identity struct.
In the one and only current auth method (type=kubernetes) all of the
trusted identity attributes are both selectable and projectable, so they
were just passed around as a map[string]string.

When namespaces were added, this was slightly changed so that the
enterprise metadata can also come back from the login operation, so
login now returned two fields.

Now with some upcoming auth methods it won't be true that all identity
attributes will be both selectable and projectable, so rather than
update the login function to return 3 pieces of data it seemed worth it
to wrap those fields up and give them a proper name.
This commit is contained in:
R.B. Boyer 2020-05-01 17:35:28 -05:00 committed by GitHub
parent 1697971a8f
commit 3ac5a841ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 290 additions and 127 deletions

View File

@ -42,7 +42,7 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
// A list of role links and service identities are returned.
func (s *Server) evaluateRoleBindings(
validator authmethod.Validator,
verifiedFields map[string]string,
verifiedIdentity *authmethod.Identity,
methodMeta *structs.EnterpriseMeta,
targetMeta *structs.EnterpriseMeta,
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
@ -54,13 +54,10 @@ func (s *Server) evaluateRoleBindings(
return nil, nil, nil
}
// Convert the fields into something suitable for go-bexpr.
selectableVars := validator.MakeFieldMapSelectable(verifiedFields)
// Find all binding rules that match the provided fields.
var matchingRules []*structs.ACLBindingRule
for _, rule := range rules {
if doesBindingRuleMatch(rule, selectableVars) {
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
matchingRules = append(matchingRules, rule)
}
}
@ -74,7 +71,7 @@ func (s *Server) evaluateRoleBindings(
serviceIdentities []*structs.ACLServiceIdentity
)
for _, rule := range matchingRules {
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedFields)
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
if err != nil {
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
} else if !valid {
@ -107,14 +104,13 @@ func (s *Server) evaluateRoleBindings(
return serviceIdentities, roleLinks, nil
}
// doesBindingRuleMatch checks that a single binding rule matches the provided
// vars.
func doesBindingRuleMatch(rule *structs.ACLBindingRule, selectableVars interface{}) bool {
if rule.Selector == "" {
// doesSelectorMatch checks that a single selector matches the provided vars.
func doesSelectorMatch(selector string, selectableVars interface{}) bool {
if selector == "" {
return true // catch-all
}
eval, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
eval, err := bexpr.CreateEvaluatorForType(selector, nil, selectableVars)
if err != nil {
return false // fails to match if selector is invalid
}

View File

@ -3,11 +3,10 @@ package consul
import (
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestDoesBindingRuleMatch(t *testing.T) {
func TestDoesSelectorMatch(t *testing.T) {
type matchable struct {
A string `bexpr:"a"`
C string `bexpr:"c"`
@ -40,8 +39,7 @@ func TestDoesBindingRuleMatch(t *testing.T) {
"", &matchable{A: "b"}, true},
} {
t.Run(test.name, func(t *testing.T) {
rule := structs.ACLBindingRule{Selector: test.selector}
ok := doesBindingRuleMatch(&rule, test.details)
ok := doesSelectorMatch(test.selector, test.details)
require.Equal(t, test.ok, ok)
})
}

View File

@ -1,6 +1,7 @@
package consul
import (
"context"
"encoding/json"
"errors"
"fmt"
@ -685,13 +686,13 @@ func validateBindingRuleBindName(bindType, bindName string, availableFields []st
}
// computeBindingRuleBindName processes the HIL for the provided bind type+name
// using the verified fields.
// using the projected variables.
//
// - If the HIL is invalid ("", false, AN_ERROR) is returned.
// - If the computed name is not valid for the type ("INVALID_NAME", false, nil) is returned.
// - If the computed name is valid for the type ("VALID_NAME", true, nil) is returned.
func computeBindingRuleBindName(bindType, bindName string, verifiedFields map[string]string) (string, bool, error) {
bindName, err := InterpolateHIL(bindName, verifiedFields)
func computeBindingRuleBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
bindName, err := InterpolateHIL(bindName, projectedVars, true)
if err != nil {
return "", false, err
}
@ -1870,10 +1871,11 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
return err
}
// Create a blank placeholder identity for use in validation below.
blankID := validator.NewIdentity()
if rule.Selector != "" {
selectableVars := validator.MakeFieldMapSelectable(map[string]string{})
_, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
if err != nil {
if _, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, blankID.SelectableFields); err != nil {
return fmt.Errorf("invalid Binding Rule: Selector is invalid: %v", err)
}
}
@ -1893,7 +1895,7 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
return fmt.Errorf("Invalid Binding Rule: unknown BindType %q", rule.BindType)
}
if valid, err := validateBindingRuleBindName(rule.BindType, rule.BindName, validator.AvailableFields()); err != nil {
if valid, err := validateBindingRuleBindName(rule.BindType, rule.BindName, blankID.ProjectedVarNames()); err != nil {
return fmt.Errorf("Invalid Binding Rule: invalid BindName: %v", err)
} else if !valid {
return fmt.Errorf("Invalid Binding Rule: invalid BindName")
@ -1909,7 +1911,7 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
}
if respErr, ok := resp.(error); ok {
return respErr
return fmt.Errorf("Failed to apply binding rule upsert request: %v", respErr)
}
if _, rule, err := a.srv.fsm.State().ACLBindingRuleGetByID(nil, rule.ID, &rule.EnterpriseMeta); err == nil && rule != nil {
@ -2283,16 +2285,16 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
}
// 2. Send args.Data.BearerToken to method validator and get back a fields map
verifiedFields, desiredMeta, err := validator.ValidateLogin(auth.BearerToken)
verifiedIdentity, err := validator.ValidateLogin(context.Background(), auth.BearerToken)
if err != nil {
return err
}
// This always will return a valid pointer
targetMeta := method.TargetEnterpriseMeta(desiredMeta)
targetMeta := method.TargetEnterpriseMeta(verifiedIdentity.EnterpriseMeta)
// 3. send map through role bindings
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedFields, &auth.EnterpriseMeta, targetMeta)
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, &auth.EnterpriseMeta, targetMeta)
if err != nil {
return err
}

View File

@ -1,6 +1,7 @@
package authmethod
import (
"context"
"fmt"
"sort"
"sync"
@ -31,6 +32,9 @@ type Validator interface {
// Name returns the name of the auth method backing this validator.
Name() string
// NewIdentity creates a blank identity populated with empty values.
NewIdentity() *Identity
// ValidateLogin takes raw user-provided auth method metadata and ensures
// it is sane, provably correct, and currently valid. Relevant identifying
// data is extracted and returned for immediate use by the role binding
@ -42,16 +46,32 @@ type Validator interface {
// Returns auth method specific metadata suitable for the Role Binding
// process as well as the desired enterprise meta for the token to be
// created.
ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error)
ValidateLogin(ctx context.Context, loginToken string) (*Identity, error)
// AvailableFields returns a slice of all fields that are returned as a
// result of ValidateLogin. These are valid fields for use in any
// BindingRule tied to this auth method.
AvailableFields() []string
// Stop should be called to cease any background activity and free up
// resources.
Stop()
}
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
// into a structure suitable for selection with a binding rule.
MakeFieldMapSelectable(fieldMap map[string]string) interface{}
type Identity struct {
// SelectableFields is the format of this Identity suitable for selection
// with a binding rule.
SelectableFields interface{}
// ProjectedVars is the format of this Identity suitable for interpolation
// in a bind name within a binding rule.
ProjectedVars map[string]string
*structs.EnterpriseMeta
}
// ProjectedVarNames returns just the keyspace of the ProjectedVars map.
func (i *Identity) ProjectedVarNames() []string {
v := make([]string, 0, len(i.ProjectedVars))
for k, _ := range i.ProjectedVars {
v = append(v, k)
}
return v
}
var (
@ -116,6 +136,7 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
if prev.ModifyIndex >= idx {
return prev.Validator
}
prev.Validator.Stop()
}
c.entries[method.Name] = &authMethodValidatorEntry{
@ -126,6 +147,9 @@ func (c *authMethodCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, val
}
func (c *authMethodCache) Purge() {
for _, entry := range c.entries {
entry.Validator.Stop()
}
c.entries = make(map[string]*authMethodValidatorEntry)
}

View File

@ -33,6 +33,6 @@ func (c *syncCache) PutValidatorIfNewer(method *structs.ACLAuthMethod, validator
func (c *syncCache) Purge() {
c.lock.Lock()
defer c.lock.Unlock()
c.cache.Purge()
c.lock.Unlock()
}

View File

@ -1,6 +1,7 @@
package kubeauth
import (
"context"
"errors"
"fmt"
"strings"
@ -21,7 +22,7 @@ import (
func init() {
// register this as an available auth method type
authmethod.Register("kubernetes", func(_ hclog.Logger, method *structs.ACLAuthMethod) (authmethod.Validator, error) {
authmethod.Register("kubernetes", func(logger hclog.Logger, method *structs.ACLAuthMethod) (authmethod.Validator, error) {
v, err := NewValidator(method)
if err != nil {
return nil, err
@ -119,9 +120,11 @@ func NewValidator(method *structs.ACLAuthMethod) (*Validator, error) {
func (v *Validator) Name() string { return v.name }
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
func (v *Validator) Stop() {}
func (v *Validator) ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error) {
if _, err := jwt.ParseSigned(loginToken); err != nil {
return nil, nil, fmt.Errorf("failed to parse and validate JWT: %v", err)
return nil, fmt.Errorf("failed to parse and validate JWT: %v", err)
}
// Check TokenReview for the bulk of the work.
@ -132,24 +135,24 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
})
if err != nil {
return nil, nil, err
return nil, err
} else if trResp.Status.Error != "" {
return nil, nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
return nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
}
if !trResp.Status.Authenticated {
return nil, nil, errors.New("lookup failed: service account jwt not valid")
return nil, errors.New("lookup failed: service account jwt not valid")
}
// The username is of format: system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)
parts := strings.Split(trResp.Status.User.Username, ":")
if len(parts) != 4 {
return nil, nil, errors.New("lookup failed: unexpected username format")
return nil, errors.New("lookup failed: unexpected username format")
}
// Validate the user that comes back from token review is a service account
if parts[0] != "system" || parts[1] != "serviceaccount" {
return nil, nil, errors.New("lookup failed: username returned is not a service account")
return nil, errors.New("lookup failed: username returned is not a service account")
}
var (
@ -161,7 +164,7 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
// Check to see if there is an override name on the ServiceAccount object.
sa, err := v.saGetter.ServiceAccounts(saNamespace).Get(saName, client_metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("annotation lookup failed: %v", err)
return nil, fmt.Errorf("annotation lookup failed: %v", err)
}
annotations := sa.GetObjectMeta().GetAnnotations()
@ -175,25 +178,37 @@ func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *struct
serviceAccountUIDField: saUID,
}
return fields, v.k8sEntMetaFromFields(fields), nil
id := v.NewIdentity()
id.SelectableFields = &k8sFieldDetails{
ServiceAccount: k8sFieldDetailsServiceAccount{
Namespace: fields[serviceAccountNamespaceField],
Name: fields[serviceAccountNameField],
UID: fields[serviceAccountUIDField],
},
}
for k, val := range fields {
id.ProjectedVars[k] = val
}
id.EnterpriseMeta = v.k8sEntMetaFromFields(fields)
return id, nil
}
func (p *Validator) AvailableFields() []string {
return []string{
func (v *Validator) NewIdentity() *authmethod.Identity {
id := &authmethod.Identity{
SelectableFields: &k8sFieldDetails{},
ProjectedVars: map[string]string{},
}
for _, f := range availableFields {
id.ProjectedVars[f] = ""
}
return id
}
var availableFields = []string{
serviceAccountNamespaceField,
serviceAccountNameField,
serviceAccountUIDField,
}
}
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
return &k8sFieldDetails{
ServiceAccount: k8sFieldDetailsServiceAccount{
Namespace: fieldMap[serviceAccountNamespaceField],
Name: fieldMap[serviceAccountNameField],
UID: fieldMap[serviceAccountUIDField],
},
}
}
type k8sFieldDetails struct {

View File

@ -7,5 +7,5 @@ import "github.com/hashicorp/consul/agent/structs"
type enterpriseConfig struct{}
func (v *Validator) k8sEntMetaFromFields(fields map[string]string) *structs.EnterpriseMeta {
return structs.DefaultEnterpriseMeta()
return nil
}

View File

@ -2,6 +2,7 @@ package kubeauth
import (
"bytes"
"context"
"testing"
"github.com/hashicorp/consul/agent/connect"
@ -74,6 +75,35 @@ func TestStructs_ACLAuthMethod_Kubernetes_MsgpackEncodeDecode(t *testing.T) {
})
}
func TestNewIdentity(t *testing.T) {
testSrv := StartTestAPIServer(t)
defer testSrv.Stop()
method := &structs.ACLAuthMethod{
Name: "test-k8s",
Description: "k8s test",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": testSrv.Addr(),
"CACert": testSrv.CACert(),
"ServiceAccountJWT": goodJWT_A,
},
}
validator, err := NewValidator(method)
require.NoError(t, err)
id := validator.NewIdentity()
authmethod.RequireIdentityMatch(t, id, map[string]string{
"serviceaccount.namespace": "",
"serviceaccount.name": "",
"serviceaccount.uid": "",
},
`serviceaccount.namespace == ""`,
`serviceaccount.name == ""`,
`serviceaccount.uid == ""`,
)
}
func TestValidateLogin(t *testing.T) {
testSrv := StartTestAPIServer(t)
defer testSrv.Stop()
@ -101,18 +131,23 @@ func TestValidateLogin(t *testing.T) {
require.NoError(t, err)
t.Run("invalid bearer token", func(t *testing.T) {
_, _, err := validator.ValidateLogin("invalid")
_, err := validator.ValidateLogin(context.Background(), "invalid")
require.Error(t, err)
})
t.Run("valid bearer token", func(t *testing.T) {
fields, _, err := validator.ValidateLogin(goodJWT_B)
id, err := validator.ValidateLogin(context.Background(), goodJWT_B)
require.NoError(t, err)
require.Equal(t, map[string]string{
authmethod.RequireIdentityMatch(t, id, map[string]string{
"serviceaccount.namespace": "default",
"serviceaccount.name": "demo",
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
}, fields)
},
`serviceaccount.namespace == default`,
`serviceaccount.name == "demo"`,
`serviceaccount.uid == "76091af4-4b56-11e9-ac4b-708b11801cbe"`,
)
})
// annotate the account
@ -125,13 +160,18 @@ func TestValidateLogin(t *testing.T) {
)
t.Run("valid bearer token with annotation", func(t *testing.T) {
fields, _, err := validator.ValidateLogin(goodJWT_B)
id, err := validator.ValidateLogin(context.Background(), goodJWT_B)
require.NoError(t, err)
require.Equal(t, map[string]string{
authmethod.RequireIdentityMatch(t, id, map[string]string{
"serviceaccount.namespace": "default",
"serviceaccount.name": "alternate-name",
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
}, fields)
},
`serviceaccount.namespace == default`,
`serviceaccount.name == "alternate-name"`,
`serviceaccount.uid == "76091af4-4b56-11e9-ac4b-708b11801cbe"`,
)
})
}

View File

@ -1,6 +1,7 @@
package testauth
import (
"context"
"fmt"
"sync"
@ -115,6 +116,8 @@ type Validator struct {
func (v *Validator) Name() string { return v.name }
func (v *Validator) Stop() {}
// ValidateLogin takes raw user-provided auth method metadata and ensures it is
// sane, provably correct, and currently valid. Relevant identifying data is
// extracted and returned for immediate use by the role binding process.
@ -123,16 +126,40 @@ func (v *Validator) Name() string { return v.name }
// to extend the life of the underlying token.
//
// Returns auth method specific metadata suitable for the Role Binding process.
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
func (v *Validator) ValidateLogin(ctx context.Context, loginToken string) (*authmethod.Identity, error) {
fields, valid := GetSessionToken(v.config.SessionID, loginToken)
if !valid {
return nil, nil, acl.ErrNotFound
return nil, acl.ErrNotFound
}
return fields, v.testAuthEntMetaFromFields(fields), nil
id := v.NewIdentity()
id.SelectableFields = &selectableVars{
ServiceAccount: selectableServiceAccount{
Namespace: fields[serviceAccountNamespaceField],
Name: fields[serviceAccountNameField],
UID: fields[serviceAccountUIDField],
},
}
for k, val := range fields {
id.ProjectedVars[k] = val
}
id.EnterpriseMeta = v.testAuthEntMetaFromFields(fields)
return id, nil
}
func (v *Validator) AvailableFields() []string { return availableFields }
func (v *Validator) NewIdentity() *authmethod.Identity {
id := &authmethod.Identity{
SelectableFields: &selectableVars{},
ProjectedVars: map[string]string{},
}
for _, f := range availableFields {
id.ProjectedVars[f] = ""
}
return id
}
const (
serviceAccountNamespaceField = "serviceaccount.namespace"
@ -146,18 +173,6 @@ var availableFields = []string{
serviceAccountUIDField,
}
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
// into a structure suitable for selection with a binding rule.
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
return &selectableVars{
ServiceAccount: selectableServiceAccount{
Namespace: fieldMap[serviceAccountNamespaceField],
Name: fieldMap[serviceAccountNameField],
UID: fieldMap[serviceAccountUIDField],
},
}
}
type selectableVars struct {
ServiceAccount selectableServiceAccount `bexpr:"serviceaccount"`
}

View File

@ -0,0 +1,45 @@
package authmethod
import (
"sort"
"github.com/hashicorp/go-bexpr"
"github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
)
// RequireIdentityMatch tests to see if the given Identity matches the provided
// projected vars and filters for testing purpose.
func RequireIdentityMatch(t testing.T, id *Identity, projectedVars map[string]string, filters ...string) {
t.Helper()
gotNames := id.ProjectedVarNames()
require.Equal(t, projectedVars, id.ProjectedVars)
expectNames := make([]string, 0, len(projectedVars))
for k, _ := range projectedVars {
expectNames = append(expectNames, k)
}
sort.Strings(expectNames)
sort.Strings(gotNames)
require.Equal(t, expectNames, gotNames)
require.Nil(t, id.EnterpriseMeta)
for _, filter := range filters {
eval, err := bexpr.CreateEvaluatorForType(filter, nil, id.SelectableFields)
if err != nil {
t.Fatalf("filter %q got err: %v", filter, err)
}
result, err := eval.Evaluate(id.SelectableFields)
if err != nil {
t.Fatalf("filter %q got err: %v", filter, err)
}
if !result {
t.Fatalf("filter %q did not match", filter)
}
}
}

View File

@ -443,7 +443,7 @@ func ServersGetACLMode(provider checkServersProvider, leaderAddr string, datacen
// InterpolateHIL processes the string as if it were HIL and interpolates only
// the provided string->string map as possible variables.
func InterpolateHIL(s string, vars map[string]string) (string, error) {
func InterpolateHIL(s string, vars map[string]string, lowercase bool) (string, error) {
if strings.Index(s, "${") == -1 {
// Skip going to the trouble of parsing something that has no HIL.
return s, nil
@ -456,6 +456,9 @@ func InterpolateHIL(s string, vars map[string]string) (string, error) {
vm := make(map[string]ast.Variable)
for k, v := range vars {
if lowercase {
v = strings.ToLower(v)
}
vm[k] = ast.Variable{
Type: ast.TypeString,
Value: v,

View File

@ -441,124 +441,139 @@ func TestServersInDCMeetMinimumVersion(t *testing.T) {
}
func TestInterpolateHIL(t *testing.T) {
for _, test := range []struct {
name string
for name, test := range map[string]struct {
in string
vars map[string]string
exp string
exp string // when lower=false
expLower string // when lower=true
ok bool
}{
// valid HIL
{
"empty",
"empty": {
"",
map[string]string{},
"",
"",
true,
},
{
"no vars",
"no vars": {
"nothing",
map[string]string{},
"nothing",
"nothing",
true,
},
{
"just var",
"just lowercase var": {
"${item}",
map[string]string{"item": "value"},
"value",
"value",
true,
},
{
"var in middle",
"just uppercase var": {
"${item}",
map[string]string{"item": "VaLuE"},
"VaLuE",
"value",
true,
},
"lowercase var in middle": {
"before ${item}after",
map[string]string{"item": "value"},
"before valueafter",
"before valueafter",
true,
},
{
"two vars",
"uppercase var in middle": {
"before ${item}after",
map[string]string{"item": "VaLuE"},
"before VaLuEafter",
"before valueafter",
true,
},
"two vars": {
"before ${item}after ${more}",
map[string]string{"item": "value", "more": "xyz"},
"before valueafter xyz",
"before valueafter xyz",
true,
},
{
"missing map val",
"missing map val": {
"${item}",
map[string]string{"item": ""},
"",
"",
true,
},
// "weird" HIL, but not technically invalid
{
"just end",
"just end": {
"}",
map[string]string{},
"}",
"}",
true,
},
{
"var without start",
"var without start": {
" item }",
map[string]string{"item": "value"},
" item }",
" item }",
true,
},
{
"two vars missing second start",
"two vars missing second start": {
"before ${ item }after more }",
map[string]string{"item": "value", "more": "xyz"},
"before valueafter more }",
"before valueafter more }",
true,
},
// invalid HIL
{
"just start",
"just start": {
"${",
map[string]string{},
"",
"",
false,
},
{
"backwards",
"backwards": {
"}${",
map[string]string{},
"",
"",
false,
},
{
"no varname",
"no varname": {
"${}",
map[string]string{},
"",
"",
false,
},
{
"missing map key",
"missing map key": {
"${item}",
map[string]string{},
"",
false,
},
{
"var without end",
"${ item ",
map[string]string{"item": "value"},
"",
false,
},
{
"two vars missing first end",
"var without end": {
"${ item ",
map[string]string{"item": "value"},
"",
"",
false,
},
"two vars missing first end": {
"before ${ item after ${ more }",
map[string]string{"item": "value", "more": "xyz"},
"",
"",
false,
},
} {
t.Run(test.name, func(t *testing.T) {
out, err := InterpolateHIL(test.in, test.vars)
test := test
t.Run(name+" lower=false", func(t *testing.T) {
out, err := InterpolateHIL(test.in, test.vars, false)
if test.ok {
require.NoError(t, err)
require.Equal(t, test.exp, out)
@ -567,6 +582,16 @@ func TestInterpolateHIL(t *testing.T) {
require.Equal(t, out, "")
}
})
t.Run(name+" lower=true", func(t *testing.T) {
out, err := InterpolateHIL(test.in, test.vars, true)
if test.ok {
require.NoError(t, err)
require.Equal(t, test.expLower, out)
} else {
require.NotNil(t, err)
require.Equal(t, out, "")
}
})
}
}