gitee.com/zhaochuninhefei/fabric-ca-gm@v0.0.2/lib/server/ldap/client.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package ldap
     8  
     9  import (
    10  	"fmt"
    11  	"net"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"gitee.com/zhaochuninhefei/fabric-ca-gm/internal/pkg/api"
    17  	"gitee.com/zhaochuninhefei/fabric-ca-gm/internal/pkg/util"
    18  	ldap "gitee.com/zhaochuninhefei/fabric-ca-gm/ldap_v2"
    19  	causer "gitee.com/zhaochuninhefei/fabric-ca-gm/lib/server/user"
    20  	"gitee.com/zhaochuninhefei/fabric-ca-gm/lib/spi"
    21  	ctls "gitee.com/zhaochuninhefei/fabric-ca-gm/lib/tls"
    22  	"gitee.com/zhaochuninhefei/fabric-gm/bccsp"
    23  	log "gitee.com/zhaochuninhefei/zcgolog/zclog"
    24  	"github.com/Knetic/govaluate"
    25  	"github.com/jmoiron/sqlx"
    26  	"github.com/pkg/errors"
    27  )
    28  
    29  var (
    30  	errNotSupported = errors.New("Not supported")
    31  )
    32  
    33  // Config is the configuration object for this LDAP client
    34  type Config struct {
    35  	Enabled     bool   `def:"false" help:"Enable the LDAP client for authentication and attributes"`
    36  	URL         string `help:"LDAP client URL of form ldap://adminDN:adminPassword@host[:port]/base" mask:"url"`
    37  	UserFilter  string `def:"(uid=%s)" help:"The LDAP user filter to use when searching for users"`
    38  	GroupFilter string `def:"(memberUid=%s)" help:"The LDAP group filter for a single affiliation group"`
    39  	Attribute   AttrConfig
    40  	TLS         ctls.ClientTLSConfig
    41  }
    42  
    43  // AttrConfig is attribute configuration information
    44  type AttrConfig struct {
    45  	Names      []string             `help:"The names of LDAP attributes to request on an LDAP search"`
    46  	Converters []NameVal            // Used to convert an LDAP entry into a fabric-ca-server attribute
    47  	Maps       map[string][]NameVal // Use to map an LDAP response to fabric-ca-server names
    48  }
    49  
    50  // NameVal is a name and value pair
    51  type NameVal struct {
    52  	Name  string
    53  	Value string
    54  }
    55  
    56  // Implements Stringer interface for ldap.Config
    57  // Calls util.StructToString to convert the Config struct to
    58  // string.
    59  func (c Config) String() string {
    60  	return util.StructToString(&c)
    61  }
    62  
    63  // NewClient creates an LDAP client
    64  func NewClient(cfg *Config, csp bccsp.BCCSP) (*Client, error) {
    65  	log.Debugf("Creating new LDAP client for %+v", cfg)
    66  	if cfg == nil {
    67  		return nil, errors.New("LDAP configuration is nil")
    68  	}
    69  	if cfg.URL == "" {
    70  		return nil, errors.New("LDAP configuration requires a 'URL'")
    71  	}
    72  	u, err := url.Parse(cfg.URL)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	var defaultPort string
    77  	switch u.Scheme {
    78  	case "ldap":
    79  		defaultPort = "389"
    80  	case "ldaps":
    81  		defaultPort = "636"
    82  	default:
    83  		return nil, errors.Errorf("Invalid LDAP scheme: %s", u.Scheme)
    84  	}
    85  	var host, port string
    86  	if !strings.Contains(u.Host, ":") {
    87  		host = u.Host
    88  		port = defaultPort
    89  	} else {
    90  		host, port, err = net.SplitHostPort(u.Host)
    91  		if err != nil {
    92  			return nil, errors.Wrapf(err, "Invalid LDAP host:port (%s)", u.Host)
    93  		}
    94  	}
    95  	portVal, err := strconv.Atoi(port)
    96  	if err != nil {
    97  		return nil, errors.Wrapf(err, "Invalid LDAP port (%s)", port)
    98  	}
    99  	c := new(Client)
   100  	c.Host = host
   101  	c.Port = portVal
   102  	c.UseSSL = u.Scheme == "ldaps"
   103  	if u.User != nil {
   104  		c.AdminDN = u.User.Username()
   105  		c.AdminPassword, _ = u.User.Password()
   106  	}
   107  	c.Base = u.Path
   108  	if c.Base != "" && strings.HasPrefix(c.Base, "/") {
   109  		c.Base = c.Base[1:]
   110  	}
   111  	c.UserFilter = cfgVal(cfg.UserFilter, "(uid=%s)")
   112  	c.GroupFilter = cfgVal(cfg.GroupFilter, "(memberUid=%s)")
   113  	c.attrNames = cfg.Attribute.Names
   114  	c.attrExprs = map[string]*userExpr{}
   115  	for _, ele := range cfg.Attribute.Converters {
   116  		ue, err := newUserExpr(c, ele.Name, ele.Value)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		c.attrExprs[ele.Name] = ue
   121  		log.Debugf("Added LDAP mapping expression for attribute '%s'", ele.Name)
   122  	}
   123  	c.attrMaps = map[string]map[string]string{}
   124  	for mapName, value := range cfg.Attribute.Maps {
   125  		c.attrMaps[mapName] = map[string]string{}
   126  		for _, ele := range value {
   127  			c.attrMaps[mapName][ele.Name] = ele.Value
   128  			log.Debugf("Added '%s' -> '%s' to LDAP map '%s'", ele.Name, ele.Value, mapName)
   129  		}
   130  	}
   131  	c.TLS = &cfg.TLS
   132  	c.CSP = csp
   133  	log.Debug("LDAP client was successfully created")
   134  	return c, nil
   135  }
   136  
   137  func cfgVal(val1, val2 string) string {
   138  	if val1 != "" {
   139  		return val1
   140  	}
   141  	return val2
   142  }
   143  
   144  // Client is an LDAP client
   145  type Client struct {
   146  	Host          string
   147  	Port          int
   148  	UseSSL        bool
   149  	AdminDN       string
   150  	AdminPassword string
   151  	Base          string
   152  	UserFilter    string               // e.g. "(uid=%s)"
   153  	GroupFilter   string               // e.g. "(memberUid=%s)"
   154  	attrNames     []string             // Names of attributes to request on an LDAP search
   155  	attrExprs     map[string]*userExpr // Expressions to evaluate to get attribute value
   156  	attrMaps      map[string]map[string]string
   157  	AdminConn     *ldap.Conn
   158  	TLS           *ctls.ClientTLSConfig
   159  	CSP           bccsp.BCCSP
   160  }
   161  
   162  // GetUser returns a user object for username and attribute values
   163  // for the requested attribute names
   164  func (lc *Client) GetUser(username string, attrNames []string) (causer.User, error) {
   165  
   166  	var sresp *ldap.SearchResult
   167  	var err error
   168  
   169  	log.Debugf("Getting user '%s'", username)
   170  
   171  	// Search for the given username
   172  	sreq := ldap.NewSearchRequest(
   173  		lc.Base, ldap.ScopeWholeSubtree,
   174  		ldap.NeverDerefAliases, 0, 0, false,
   175  		fmt.Sprintf(lc.UserFilter, username),
   176  		lc.attrNames,
   177  		nil,
   178  	)
   179  
   180  	// Try to search using the cached connection, if there is one
   181  	conn := lc.AdminConn
   182  	if conn != nil {
   183  		log.Debugf("Searching for user '%s' using cached connection", username)
   184  		sresp, err = conn.Search(sreq)
   185  		if err != nil {
   186  			log.Debugf("LDAP search failed but will close connection and try again; error was: %s", err)
   187  			conn.Close()
   188  			lc.AdminConn = nil
   189  		}
   190  	}
   191  
   192  	// If there was no cached connection or the search failed for any reason
   193  	// (including because the server may have closed the cached connection),
   194  	// try with a new connection.
   195  	if sresp == nil {
   196  		log.Debugf("Searching for user '%s' using new connection", username)
   197  		conn, err = lc.newConnection()
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		sresp, err = conn.Search(sreq)
   202  		if err != nil {
   203  			conn.Close()
   204  			return nil, errors.Wrapf(err, "LDAP search failure; search request: %+v", sreq)
   205  		}
   206  		// Cache the connection
   207  		lc.AdminConn = conn
   208  	}
   209  
   210  	// Make sure there was exactly one match found
   211  	if len(sresp.Entries) < 1 {
   212  		return nil, errors.Errorf("User '%s' does not exist in LDAP directory", username)
   213  	}
   214  	if len(sresp.Entries) > 1 {
   215  		return nil, errors.Errorf("Multiple users with name '%s' exist in LDAP directory", username)
   216  	}
   217  
   218  	entry := sresp.Entries[0]
   219  	if entry == nil {
   220  		return nil, errors.Errorf("No entry was returned for user '%s'", username)
   221  	}
   222  
   223  	// Construct the user object
   224  	user := &user{
   225  		name:   username,
   226  		entry:  entry,
   227  		client: lc,
   228  	}
   229  
   230  	log.Debugf("Successfully retrieved user '%s', DN: %s", username, entry.DN)
   231  
   232  	return user, nil
   233  }
   234  
   235  // InsertUser inserts a user
   236  func (lc *Client) InsertUser(user *causer.Info) error {
   237  	return errNotSupported
   238  }
   239  
   240  // UpdateUser updates a user
   241  func (lc *Client) UpdateUser(user *causer.Info, updatePass bool) error {
   242  	return errNotSupported
   243  }
   244  
   245  // DeleteUser deletes a user
   246  func (lc *Client) DeleteUser(id string) (causer.User, error) {
   247  	return nil, errNotSupported
   248  }
   249  
   250  // GetAffiliation returns an affiliation group
   251  func (lc *Client) GetAffiliation(name string) (spi.Affiliation, error) {
   252  	return nil, errNotSupported
   253  }
   254  
   255  // GetAllAffiliations gets affiliation and any sub affiliation from the database
   256  func (lc *Client) GetAllAffiliations(name string) (*sqlx.Rows, error) {
   257  	return nil, errNotSupported
   258  }
   259  
   260  // GetRootAffiliation returns the root affiliation group
   261  func (lc *Client) GetRootAffiliation() (spi.Affiliation, error) {
   262  	return nil, errNotSupported
   263  }
   264  
   265  // InsertAffiliation adds an affiliation group
   266  func (lc *Client) InsertAffiliation(name string, prekey string, version int) error {
   267  	return errNotSupported
   268  }
   269  
   270  // DeleteAffiliation deletes an affiliation group
   271  func (lc *Client) DeleteAffiliation(name string, force, identityRemoval, isRegistrar bool) (*causer.DbTxResult, error) {
   272  	return nil, errNotSupported
   273  }
   274  
   275  // ModifyAffiliation renames the affiliation and updates all identities to use the new affiliation
   276  func (lc *Client) ModifyAffiliation(oldAffiliation, newAffiliation string, force, isRegistrar bool) (*causer.DbTxResult, error) {
   277  	return nil, errNotSupported
   278  }
   279  
   280  // GetUserLessThanLevel returns all identities that are less than the level specified
   281  func (lc *Client) GetUserLessThanLevel(version int) ([]causer.User, error) {
   282  	return nil, errNotSupported
   283  }
   284  
   285  // GetFilteredUsers returns all identities that fall under the affiliation and types
   286  func (lc *Client) GetFilteredUsers(affiliation, types string) (*sqlx.Rows, error) {
   287  	return nil, errNotSupported
   288  }
   289  
   290  // GetAffiliationTree returns the requested affiliations and all affiliations below it
   291  func (lc *Client) GetAffiliationTree(name string) (*causer.DbTxResult, error) {
   292  	return nil, errNotSupported
   293  }
   294  
   295  // Connect to the LDAP server and bind as user as admin user as specified in LDAP URL
   296  func (lc *Client) newConnection() (conn *ldap.Conn, err error) {
   297  	address := fmt.Sprintf("%s:%d", lc.Host, lc.Port)
   298  	if !lc.UseSSL {
   299  		log.Debug("Connecting to LDAP server over TCP")
   300  		conn, err = ldap.Dial("tcp", address)
   301  		if err != nil {
   302  			return conn, errors.Wrapf(err, "Failed to connect to LDAP server over TCP at %s", address)
   303  		}
   304  	} else {
   305  		log.Debug("Connecting to LDAP server over TLS")
   306  		tlsConfig, err2 := ctls.GetClientTLSConfig(lc.TLS, lc.CSP)
   307  		if err2 != nil {
   308  			return nil, errors.WithMessage(err2, "Failed to get client TLS config")
   309  		}
   310  
   311  		tlsConfig.ServerName = lc.Host
   312  
   313  		conn, err = ldap.DialTLS("tcp", address, tlsConfig)
   314  		if err != nil {
   315  			return conn, errors.Wrapf(err, "Failed to connect to LDAP server over TLS at %s", address)
   316  		}
   317  	}
   318  	// Bind with a read only user
   319  	if lc.AdminDN != "" && lc.AdminPassword != "" {
   320  		log.Debugf("Binding to the LDAP server as admin user %s", lc.AdminDN)
   321  		err := conn.Bind(lc.AdminDN, lc.AdminPassword)
   322  		if err != nil {
   323  			return nil, errors.Wrapf(err, "LDAP bind failure as %s", lc.AdminDN)
   324  		}
   325  	}
   326  	return conn, nil
   327  }
   328  
   329  // A user represents a single user or identity from LDAP
   330  type user struct {
   331  	name   string
   332  	entry  *ldap.Entry
   333  	client *Client
   334  }
   335  
   336  // GetName returns the user's enrollment ID, which is the DN (Distinquished Name)
   337  func (u *user) GetName() string {
   338  	return u.entry.DN
   339  }
   340  
   341  // GetType returns the type of the user
   342  func (u *user) GetType() string {
   343  	return "client"
   344  }
   345  
   346  // GetMaxEnrollments returns the max enrollments of the user
   347  func (u *user) GetMaxEnrollments() int {
   348  	return 0
   349  }
   350  
   351  // GetLevel returns the level of the user
   352  func (u *user) GetLevel() int {
   353  	return 0
   354  }
   355  
   356  // SetLevel sets the level of the user
   357  func (u *user) SetLevel(level int) error {
   358  	return errNotSupported
   359  }
   360  
   361  // Login logs a user in using password
   362  func (u *user) Login(password string, caMaxEnrollment int) error {
   363  
   364  	// Get a connection to use to bind over as the user to check the password
   365  	conn, err := u.client.newConnection()
   366  	if err != nil {
   367  		return err
   368  	}
   369  	defer conn.Close()
   370  
   371  	// Bind calls the LDAP server to check the user's password
   372  	err = conn.Bind(u.entry.DN, password)
   373  	if err != nil {
   374  		return errors.Wrapf(err, "LDAP authentication failure for user '%s' (DN=%s)", u.name, u.entry.DN)
   375  	}
   376  
   377  	return nil
   378  
   379  }
   380  
   381  // LoginComplete requires no action on LDAP
   382  func (u *user) LoginComplete() error {
   383  	return nil
   384  }
   385  
   386  // GetAffiliationPath returns the affiliation path for this user.
   387  // We convert the OU hierarchy to an array of strings, orderered
   388  // from top-to-bottom.
   389  func (u *user) GetAffiliationPath() []string {
   390  	dn := u.entry.DN
   391  	path := []string{}
   392  	parts := strings.Split(dn, ",")
   393  	for i := len(parts) - 1; i >= 0; i-- {
   394  		p := parts[i]
   395  		if strings.HasPrefix(strings.ToUpper(p), "OU=") {
   396  			path = append(path, strings.Trim(p[3:], " "))
   397  		}
   398  	}
   399  	log.Debugf("Affiliation path for DN '%s' is '%+v'", dn, path)
   400  	return path
   401  }
   402  
   403  // GetAttribute returns the value of an attribute, or "" if not found
   404  func (u *user) GetAttribute(name string) (*api.Attribute, error) {
   405  	expr := u.client.attrExprs[name]
   406  	if expr == nil {
   407  		log.Debugf("Getting attribute '%s' from LDAP user '%s'", name, u.name)
   408  		vals := u.entry.GetAttributeValues(name)
   409  		if len(vals) == 0 {
   410  			vals = make([]string, 0)
   411  		}
   412  		return &api.Attribute{Name: name, Value: strings.Join(vals, ",")}, nil
   413  	}
   414  	log.Debugf("Evaluating expression for attribute '%s' from LDAP user '%s'", name, u.name)
   415  	value, err := expr.evaluate(u)
   416  	if err != nil {
   417  		return nil, errors.Wrap(err, "Failed to evaluate LDAP expression")
   418  	}
   419  	return &api.Attribute{Name: name, Value: fmt.Sprintf("%v", value)}, nil
   420  }
   421  
   422  // GetAttributes returns the requested attributes
   423  func (u *user) GetAttributes(attrNames []string) ([]api.Attribute, error) {
   424  	attrs := []api.Attribute{}
   425  	if attrNames == nil {
   426  		attrNames = u.client.attrNames
   427  	}
   428  	for _, name := range attrNames {
   429  		attr, err := u.GetAttribute(name)
   430  		if err != nil {
   431  			return nil, err
   432  		}
   433  		attrs = append(attrs, *attr)
   434  	}
   435  	for name := range u.client.attrExprs {
   436  		attr, err := u.GetAttribute(name)
   437  		if err != nil {
   438  			return nil, err
   439  		}
   440  		attrs = append(attrs, *attr)
   441  	}
   442  	return attrs, nil
   443  }
   444  
   445  // Revoke is not supported for LDAP
   446  func (u *user) Revoke() error {
   447  	return errNotSupported
   448  }
   449  
   450  // IsRevoked is not supported for LDAP
   451  func (u *user) IsRevoked() bool {
   452  	return false
   453  }
   454  
   455  // ModifyAttributes adds a new attribute or modifies existing attribute
   456  func (u *user) ModifyAttributes(attrs []api.Attribute) error {
   457  	return errNotSupported
   458  }
   459  
   460  // IncrementIncorrectPasswordAttempts is not supported for LDAP
   461  func (u *user) IncrementIncorrectPasswordAttempts() error {
   462  	return errNotSupported
   463  }
   464  
   465  func (u *user) GetFailedLoginAttempts() int {
   466  	return 0
   467  }
   468  
   469  func newUserExpr(client *Client, attr, expr string) (*userExpr, error) {
   470  	ue := &userExpr{client: client, attr: attr, expr: expr}
   471  	err := ue.parse()
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  	return ue, nil
   476  }
   477  
   478  type userExpr struct {
   479  	client     *Client
   480  	attr, expr string
   481  	eval       *govaluate.EvaluableExpression
   482  	user       *user
   483  }
   484  
   485  func (ue *userExpr) parse() error {
   486  	eval, err := govaluate.NewEvaluableExpression(ue.expr)
   487  	if err == nil {
   488  		// We were able to parse 'expr' without reference to any defined
   489  		// functions, so we can reuse this evaluator across multiple users.
   490  		ue.eval = eval
   491  		return nil
   492  	}
   493  	// Try to parse 'expr' with defined functions
   494  	_, err = govaluate.NewEvaluableExpressionWithFunctions(ue.expr, ue.functions())
   495  	if err != nil {
   496  		return errors.Wrapf(err, "Invalid expression for attribute '%s'", ue.attr)
   497  	}
   498  	return nil
   499  }
   500  
   501  func (ue *userExpr) evaluate(user *user) (interface{}, error) {
   502  	var err error
   503  	parms := map[string]interface{}{
   504  		"DN":          user.entry.DN,
   505  		"affiliation": user.GetAffiliationPath(),
   506  	}
   507  	eval := ue.eval
   508  	if eval == nil {
   509  		ue2 := &userExpr{
   510  			client: ue.client,
   511  			attr:   ue.attr,
   512  			expr:   ue.expr,
   513  			user:   user,
   514  		}
   515  		eval, err = govaluate.NewEvaluableExpressionWithFunctions(ue2.expr, ue2.functions())
   516  		if err != nil {
   517  			return nil, errors.Wrapf(err, "Invalid expression for attribute '%s'", ue.attr)
   518  		}
   519  	}
   520  	result, err := eval.Evaluate(parms)
   521  	if err != nil {
   522  		log.Debugf("Error evaluating expression for attribute '%s'; parms: %+v; error: %+v", ue.attr, parms, err)
   523  		return nil, err
   524  	}
   525  	log.Debugf("Evaluated expression for attribute '%s'; parms: %+v; result: %+v", ue.attr, parms, result)
   526  	return result, nil
   527  }
   528  
   529  func (ue *userExpr) functions() map[string]govaluate.ExpressionFunction {
   530  	return map[string]govaluate.ExpressionFunction{
   531  		"attr": ue.attrFunction,
   532  		"map":  ue.mapFunction,
   533  		"if":   ue.ifFunction,
   534  	}
   535  }
   536  
   537  // Get an LDAP attribute's value.
   538  // The usage is:
   539  //     attrFunction <attrName> [<separator>]
   540  // If attribute <attrName> has multiple values, return the values in a single
   541  // string separated by the <separator> string, which is a comma by default.
   542  // Example:
   543  //    Assume attribute "foo" has two values "bar1" and "bar2".
   544  //    attrFunction("foo") returns "bar1,bar2"
   545  //    attrFunction("foo",":") returns "bar1:bar2"
   546  func (ue *userExpr) attrFunction(args ...interface{}) (interface{}, error) {
   547  	if len(args) < 1 || len(args) > 2 {
   548  		return nil, fmt.Errorf("Expecting 1 or 2 arguments for 'attr' but found %d", len(args))
   549  	}
   550  	attrName, ok := args[0].(string)
   551  	if !ok {
   552  		return nil, errors.Errorf("First argument to 'attr' must be a string; '%s' is not a string", args[0])
   553  	}
   554  	sep := ","
   555  	if len(args) == 2 {
   556  		sep, ok = args[1].(string)
   557  		if !ok {
   558  			return nil, errors.Errorf("Second argument to 'attr' must be a string; '%s' is not a string", args[1])
   559  		}
   560  	}
   561  	vals := ue.user.entry.GetAttributeValues(attrName)
   562  	log.Debugf("Values for LDAP attribute '%s' are '%+v'", attrName, vals)
   563  	if len(vals) == 0 {
   564  		vals = make([]string, 0)
   565  	}
   566  	return strings.Join(vals, sep), nil
   567  }
   568  
   569  // Map function performs string substitutions on the 1st argument for each
   570  // entry in the map referenced by the 2nd argument.
   571  //
   572  // For example, assume that a user's LDAP attribute named 'myLDAPAttr' has
   573  // three values: "foo1", "foo2", and "foo3".  Further assume the following
   574  // LDAP configuration.
   575  //
   576  //    converters:
   577  //       - name: myAttr
   578  //         value: map(attr("myLDAPAttr"), myMap)
   579  //    maps:
   580  //       myMap:
   581  //          foo1: bar1
   582  //          foo2: bar2
   583  //
   584  // The value of the user's "myAttr" attribute is then "bar1,bar2,foo3".
   585  // This value is computed as follows:
   586  // 1) The value of 'attr("myLDAPAttr")' is "foo1,foo2,foo3" by joining
   587  //    the values using the default separator character ",".
   588  // 2) The value of 'map("foo1,foo2,foo3", "myMap")' is "foo1,foo2,foo3"
   589  //    because it maps or substitutes "bar1" for "foo1" and "bar2" for "foo2"
   590  //    according to the entries in the "myMap" map.
   591  func (ue *userExpr) mapFunction(args ...interface{}) (interface{}, error) {
   592  	if len(args) != 2 {
   593  		return nil, errors.Errorf("Expecting two arguments but found %d", len(args))
   594  	}
   595  	str, ok := args[0].(string)
   596  	if !ok {
   597  		return nil, errors.Errorf("First argument to 'map' must be a string; '%s' is not a string", args[0])
   598  	}
   599  	mapName := args[1].(string)
   600  	if !ok {
   601  		return nil, errors.Errorf("Second argument to 'map' must be a string; '%s' is not a string", args[1])
   602  	}
   603  	mapName = strings.ToLower(mapName)
   604  	// Get the map
   605  	maps := ue.client.attrMaps
   606  	if maps == nil {
   607  		return nil, errors.Errorf("No maps are defined; unknown map name: '%s'", mapName)
   608  	}
   609  	myMap := maps[mapName]
   610  	if myMap == nil {
   611  		return nil, errors.Errorf("Unknown map name: '%s'", mapName)
   612  	}
   613  	// Iterate through all of the entries in the map and perform string substitution
   614  	// from the name to the value.
   615  	for name, val := range myMap {
   616  		str = strings.Replace(str, name, val, -1)
   617  	}
   618  	return str, nil
   619  }
   620  
   621  // The "ifFunction" returns the 2nd arg if the 1st boolean arg is true; otherwise it
   622  // returns the 3rd arg.
   623  func (ue *userExpr) ifFunction(args ...interface{}) (interface{}, error) {
   624  	if len(args) != 3 {
   625  		return nil, fmt.Errorf("Expecting 3 arguments for 'if' but found %d", len(args))
   626  	}
   627  	cond, ok := args[0].(bool)
   628  	if !ok {
   629  		return nil, errors.New("Expecting first argument to 'if' to be a boolean")
   630  	}
   631  	if cond {
   632  		return args[1], nil
   633  	}
   634  	return args[2], nil
   635  }