github.com/silveraid/fabric-ca@v1.1.0-preview.0.20180127000700-71974f53ab08/lib/ldap/client.go (about)

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