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