github.com/hashicorp/vault/sdk@v0.11.0/helper/ldaputil/client.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package ldaputil
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"encoding/binary"
    11  	"encoding/hex"
    12  	"fmt"
    13  	"math"
    14  	"net"
    15  	"net/url"
    16  	"strings"
    17  	"sync"
    18  	"text/template"
    19  	"time"
    20  
    21  	"github.com/go-ldap/ldap/v3"
    22  	hclog "github.com/hashicorp/go-hclog"
    23  	multierror "github.com/hashicorp/go-multierror"
    24  	"github.com/hashicorp/go-secure-stdlib/tlsutil"
    25  )
    26  
    27  type Client struct {
    28  	Logger hclog.Logger
    29  	LDAP   LDAP
    30  }
    31  
    32  func (c *Client) DialLDAP(cfg *ConfigEntry) (Connection, error) {
    33  	var retErr *multierror.Error
    34  	var conn Connection
    35  	urls := strings.Split(cfg.Url, ",")
    36  
    37  	for _, uut := range urls {
    38  		u, err := url.Parse(uut)
    39  		if err != nil {
    40  			retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error parsing url %q: {{err}}", uut), err))
    41  			continue
    42  		}
    43  		host, port, err := net.SplitHostPort(u.Host)
    44  		if err != nil {
    45  			host = u.Host
    46  		}
    47  
    48  		var tlsConfig *tls.Config
    49  		dialer := net.Dialer{
    50  			Timeout: time.Duration(cfg.ConnectionTimeout) * time.Second,
    51  		}
    52  
    53  		switch u.Scheme {
    54  		case "ldap":
    55  			if port == "" {
    56  				port = "389"
    57  			}
    58  
    59  			fullAddr := fmt.Sprintf("%s://%s", u.Scheme, net.JoinHostPort(host, port))
    60  			opt := ldap.DialWithDialer(&dialer)
    61  
    62  			conn, err = c.LDAP.DialURL(fullAddr, opt)
    63  			if err != nil {
    64  				break
    65  			}
    66  			if conn == nil {
    67  				err = fmt.Errorf("empty connection after dialing")
    68  				break
    69  			}
    70  			if cfg.StartTLS {
    71  				tlsConfig, err = getTLSConfig(cfg, host)
    72  				if err != nil {
    73  					break
    74  				}
    75  				err = conn.StartTLS(tlsConfig)
    76  			}
    77  		case "ldaps":
    78  			if port == "" {
    79  				port = "636"
    80  			}
    81  			tlsConfig, err = getTLSConfig(cfg, host)
    82  			if err != nil {
    83  				break
    84  			}
    85  
    86  			fullAddr := fmt.Sprintf("%s://%s", u.Scheme, net.JoinHostPort(host, port))
    87  			opt := ldap.DialWithDialer(&dialer)
    88  			tls := ldap.DialWithTLSConfig(tlsConfig)
    89  
    90  			conn, err = c.LDAP.DialURL(fullAddr, opt, tls)
    91  			if err != nil {
    92  				break
    93  			}
    94  		default:
    95  			retErr = multierror.Append(retErr, fmt.Errorf("invalid LDAP scheme in url %q", net.JoinHostPort(host, port)))
    96  			continue
    97  		}
    98  		if err == nil {
    99  			if retErr != nil {
   100  				if c.Logger.IsDebug() {
   101  					c.Logger.Debug("errors connecting to some hosts", "error", retErr.Error())
   102  				}
   103  			}
   104  			retErr = nil
   105  			break
   106  		}
   107  		retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err))
   108  	}
   109  	if retErr != nil {
   110  		return nil, retErr
   111  	}
   112  	if timeout := cfg.RequestTimeout; timeout > 0 {
   113  		conn.SetTimeout(time.Duration(timeout) * time.Second)
   114  	}
   115  	return conn, nil
   116  }
   117  
   118  /*
   119   * Searches for a username in the ldap server, returning a minimal subset of the
   120   * user's attributes (if found)
   121   */
   122  func (c *Client) makeLdapSearchRequest(cfg *ConfigEntry, conn Connection, username string) (*ldap.SearchResult, error) {
   123  	// Note: The logic below drives the logic in ConfigEntry.Validate().
   124  	// If updated, please update there as well.
   125  	var err error
   126  	if cfg.BindPassword != "" {
   127  		err = conn.Bind(cfg.BindDN, cfg.BindPassword)
   128  	} else {
   129  		err = conn.UnauthenticatedBind(cfg.BindDN)
   130  	}
   131  	if err != nil {
   132  		return nil, fmt.Errorf("LDAP bind (service) failed: %w", err)
   133  	}
   134  
   135  	renderedFilter, err := c.RenderUserSearchFilter(cfg, username)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	if c.Logger.IsDebug() {
   141  		c.Logger.Debug("discovering user", "userdn", cfg.UserDN, "filter", renderedFilter)
   142  	}
   143  	ldapRequest := &ldap.SearchRequest{
   144  		BaseDN:       cfg.UserDN,
   145  		DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   146  		Scope:        ldap.ScopeWholeSubtree,
   147  		Filter:       renderedFilter,
   148  		SizeLimit:    2, // Should be only 1 result. Any number larger (2 or more) means access denied.
   149  		Attributes: []string{
   150  			cfg.UserAttr, // Return only needed attributes
   151  		},
   152  	}
   153  
   154  	result, err := conn.Search(ldapRequest)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	return result, nil
   160  }
   161  
   162  /*
   163   * Discover and return the bind string for the user attempting to authenticate, as well as the
   164   * value to use for the identity alias.
   165   * This is handled in one of several ways:
   166   *
   167   * 1. If DiscoverDN is set, the user object will be searched for using userdn (base search path)
   168   *    and userattr (the attribute that maps to the provided username) or user search filter.
   169   *    The bind will either be anonymous or use binddn and bindpassword if they were provided.
   170   * 2. If upndomain is set, the user dn and alias attribte are constructed as 'username@upndomain'.
   171   *    See https://msdn.microsoft.com/en-us/library/cc223499.aspx
   172   *
   173   */
   174  func (c *Client) GetUserBindDN(cfg *ConfigEntry, conn Connection, username string) (string, error) {
   175  	bindDN := ""
   176  
   177  	// Note: The logic below drives the logic in ConfigEntry.Validate().
   178  	// If updated, please update there as well.
   179  	if cfg.DiscoverDN || (cfg.BindDN != "" && cfg.BindPassword != "") {
   180  
   181  		result, err := c.makeLdapSearchRequest(cfg, conn, username)
   182  		if err != nil {
   183  			return bindDN, fmt.Errorf("LDAP search for binddn failed %w", err)
   184  		}
   185  		if len(result.Entries) != 1 {
   186  			return bindDN, fmt.Errorf("LDAP search for binddn 0 or not unique")
   187  		}
   188  
   189  		bindDN = result.Entries[0].DN
   190  
   191  	} else {
   192  		if cfg.UPNDomain != "" {
   193  			bindDN = fmt.Sprintf("%s@%s", EscapeLDAPValue(username), cfg.UPNDomain)
   194  		} else {
   195  			bindDN = fmt.Sprintf("%s=%s,%s", cfg.UserAttr, EscapeLDAPValue(username), cfg.UserDN)
   196  		}
   197  	}
   198  
   199  	return bindDN, nil
   200  }
   201  
   202  func (c *Client) RenderUserSearchFilter(cfg *ConfigEntry, username string) (string, error) {
   203  	// The UserFilter can be blank if not set, or running this version of the code
   204  	// on an existing ldap configuration
   205  	if cfg.UserFilter == "" {
   206  		cfg.UserFilter = "({{.UserAttr}}={{.Username}})"
   207  	}
   208  
   209  	// If userfilter was defined, resolve it as a Go template and use the query to
   210  	// find the login user
   211  	if c.Logger.IsDebug() {
   212  		c.Logger.Debug("compiling search filter", "search_filter", cfg.UserFilter)
   213  	}
   214  
   215  	// Parse the configuration as a template.
   216  	// Example template "({{.UserAttr}}={{.Username}})"
   217  	t, err := template.New("queryTemplate").Parse(cfg.UserFilter)
   218  	if err != nil {
   219  		return "", fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
   220  	}
   221  
   222  	// Build context to pass to template - we will be exposing UserDn and Username.
   223  	context := struct {
   224  		UserAttr string
   225  		Username string
   226  	}{
   227  		ldap.EscapeFilter(cfg.UserAttr),
   228  		ldap.EscapeFilter(username),
   229  	}
   230  	if cfg.UPNDomain != "" {
   231  		context.UserAttr = "userPrincipalName"
   232  		// Intentionally, calling EscapeFilter(...) (vs EscapeValue) since the
   233  		// username is being injected into a search filter.
   234  		// As an untrusted string, the username must be escaped according to RFC
   235  		// 4515, in order to prevent attackers from injecting characters that could modify the filter
   236  		context.Username = fmt.Sprintf("%s@%s", ldap.EscapeFilter(username), cfg.UPNDomain)
   237  	}
   238  
   239  	// Execute the template. Note that the template context contains escaped input and does
   240  	// not provide behavior via functions. Additionally, no function map has been provided
   241  	// during template initialization. The only template functions available during execution
   242  	// are the predefined global functions: https://pkg.go.dev/text/template#hdr-Functions
   243  	var renderedFilter bytes.Buffer
   244  	if err := t.Execute(&renderedFilter, context); err != nil {
   245  		return "", fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
   246  	}
   247  
   248  	return renderedFilter.String(), nil
   249  }
   250  
   251  /*
   252   * Returns the value to be used for the entity alias of this user
   253   * This is handled in one of several ways:
   254   *
   255   * 1. If DiscoverDN is set, the user will be searched for using userdn (base search path)
   256   *    and userattr (the attribute that maps to the provided username) or user search filter.
   257   *    The bind will either be anonymous or use binddn and bindpassword if they were provided.
   258   * 2. If upndomain is set, the alias attribte is constructed as 'username@upndomain'.
   259   *
   260   */
   261  func (c *Client) GetUserAliasAttributeValue(cfg *ConfigEntry, conn Connection, username string) (string, error) {
   262  	aliasAttributeValue := ""
   263  
   264  	// Note: The logic below drives the logic in ConfigEntry.Validate().
   265  	// If updated, please update there as well.
   266  	if cfg.DiscoverDN || (cfg.BindDN != "" && cfg.BindPassword != "") {
   267  
   268  		result, err := c.makeLdapSearchRequest(cfg, conn, username)
   269  		if err != nil {
   270  			return aliasAttributeValue, fmt.Errorf("LDAP search for entity alias attribute failed: %w", err)
   271  		}
   272  		if len(result.Entries) != 1 {
   273  			return aliasAttributeValue, fmt.Errorf("LDAP search for entity alias attribute 0 or not unique")
   274  		}
   275  
   276  		if len(result.Entries[0].Attributes) != 1 {
   277  			return aliasAttributeValue, fmt.Errorf("LDAP attribute missing for entity alias mapping")
   278  		}
   279  
   280  		if len(result.Entries[0].Attributes[0].Values) != 1 {
   281  			return aliasAttributeValue, fmt.Errorf("LDAP entity alias attribute %s empty or not unique for entity alias mapping", cfg.UserAttr)
   282  		}
   283  
   284  		aliasAttributeValue = result.Entries[0].Attributes[0].Values[0]
   285  	} else {
   286  		if cfg.UPNDomain != "" {
   287  			aliasAttributeValue = fmt.Sprintf("%s@%s", EscapeLDAPValue(username), cfg.UPNDomain)
   288  		} else {
   289  			aliasAttributeValue = fmt.Sprintf("%s=%s,%s", cfg.UserAttr, EscapeLDAPValue(username), cfg.UserDN)
   290  		}
   291  	}
   292  
   293  	return aliasAttributeValue, nil
   294  }
   295  
   296  /*
   297   * Returns the DN of the object representing the authenticated user.
   298   */
   299  func (c *Client) GetUserDN(cfg *ConfigEntry, conn Connection, bindDN, username string) (string, error) {
   300  	userDN := ""
   301  	if cfg.UPNDomain != "" {
   302  		// Find the distinguished name for the user if userPrincipalName used for login
   303  		filter := fmt.Sprintf("(userPrincipalName=%s@%s)", EscapeLDAPValue(username), cfg.UPNDomain)
   304  		if c.Logger.IsDebug() {
   305  			c.Logger.Debug("searching upn", "userdn", cfg.UserDN, "filter", filter)
   306  		}
   307  		result, err := conn.Search(&ldap.SearchRequest{
   308  			BaseDN:       cfg.UserDN,
   309  			Scope:        ldap.ScopeWholeSubtree,
   310  			DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   311  			Filter:       filter,
   312  			SizeLimit:    math.MaxInt32,
   313  		})
   314  		if err != nil {
   315  			return userDN, fmt.Errorf("LDAP search failed for detecting user: %w", err)
   316  		}
   317  		for _, e := range result.Entries {
   318  			userDN = e.DN
   319  		}
   320  	} else {
   321  		userDN = bindDN
   322  	}
   323  
   324  	return userDN, nil
   325  }
   326  
   327  func (c *Client) performLdapFilterGroupsSearch(cfg *ConfigEntry, conn Connection, userDN string, username string) ([]*ldap.Entry, error) {
   328  	if cfg.GroupFilter == "" {
   329  		c.Logger.Warn("groupfilter is empty, will not query server")
   330  		return make([]*ldap.Entry, 0), nil
   331  	}
   332  
   333  	if cfg.GroupDN == "" {
   334  		c.Logger.Warn("groupdn is empty, will not query server")
   335  		return make([]*ldap.Entry, 0), nil
   336  	}
   337  
   338  	// If groupfilter was defined, resolve it as a Go template and use the query for
   339  	// returning the user's groups
   340  	if c.Logger.IsDebug() {
   341  		c.Logger.Debug("compiling group filter", "group_filter", cfg.GroupFilter)
   342  	}
   343  
   344  	// Parse the configuration as a template.
   345  	// Example template "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))"
   346  	t, err := template.New("queryTemplate").Parse(cfg.GroupFilter)
   347  	if err != nil {
   348  		return nil, fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
   349  	}
   350  
   351  	// Build context to pass to template - we will be exposing UserDn and Username.
   352  	context := struct {
   353  		UserDN   string
   354  		Username string
   355  	}{
   356  		ldap.EscapeFilter(userDN),
   357  		ldap.EscapeFilter(username),
   358  	}
   359  
   360  	var renderedQuery bytes.Buffer
   361  	if err := t.Execute(&renderedQuery, context); err != nil {
   362  		return nil, fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
   363  	}
   364  
   365  	if c.Logger.IsDebug() {
   366  		c.Logger.Debug("searching", "groupdn", cfg.GroupDN, "rendered_query", renderedQuery.String())
   367  	}
   368  
   369  	result, err := conn.Search(&ldap.SearchRequest{
   370  		BaseDN:       cfg.GroupDN,
   371  		Scope:        ldap.ScopeWholeSubtree,
   372  		DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   373  		Filter:       renderedQuery.String(),
   374  		Attributes: []string{
   375  			cfg.GroupAttr,
   376  		},
   377  		SizeLimit: math.MaxInt32,
   378  	})
   379  	if err != nil {
   380  		return nil, fmt.Errorf("LDAP search failed: %w", err)
   381  	}
   382  
   383  	return result.Entries, nil
   384  }
   385  
   386  func (c *Client) performLdapFilterGroupsSearchPaging(cfg *ConfigEntry, conn PagingConnection, userDN string, username string) ([]*ldap.Entry, error) {
   387  	if cfg.GroupFilter == "" {
   388  		c.Logger.Warn("groupfilter is empty, will not query server")
   389  		return make([]*ldap.Entry, 0), nil
   390  	}
   391  
   392  	if cfg.GroupDN == "" {
   393  		c.Logger.Warn("groupdn is empty, will not query server")
   394  		return make([]*ldap.Entry, 0), nil
   395  	}
   396  
   397  	// If groupfilter was defined, resolve it as a Go template and use the query for
   398  	// returning the user's groups
   399  	if c.Logger.IsDebug() {
   400  		c.Logger.Debug("compiling group filter", "group_filter", cfg.GroupFilter)
   401  	}
   402  
   403  	// Parse the configuration as a template.
   404  	// Example template "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))"
   405  	t, err := template.New("queryTemplate").Parse(cfg.GroupFilter)
   406  	if err != nil {
   407  		return nil, fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
   408  	}
   409  
   410  	// Build context to pass to template - we will be exposing UserDn and Username.
   411  	context := struct {
   412  		UserDN   string
   413  		Username string
   414  	}{
   415  		ldap.EscapeFilter(userDN),
   416  		ldap.EscapeFilter(username),
   417  	}
   418  
   419  	// Execute the template. Note that the template context contains escaped input and does
   420  	// not provide behavior via functions. Additionally, no function map has been provided
   421  	// during template initialization. The only template functions available during execution
   422  	// are the predefined global functions: https://pkg.go.dev/text/template#hdr-Functions
   423  	var renderedQuery bytes.Buffer
   424  	if err := t.Execute(&renderedQuery, context); err != nil {
   425  		return nil, fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
   426  	}
   427  
   428  	if c.Logger.IsDebug() {
   429  		c.Logger.Debug("searching", "groupdn", cfg.GroupDN, "rendered_query", renderedQuery.String())
   430  	}
   431  
   432  	result, err := conn.SearchWithPaging(&ldap.SearchRequest{
   433  		BaseDN:       cfg.GroupDN,
   434  		Scope:        ldap.ScopeWholeSubtree,
   435  		DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   436  		Filter:       renderedQuery.String(),
   437  		Attributes: []string{
   438  			cfg.GroupAttr,
   439  		},
   440  		SizeLimit: math.MaxInt32,
   441  	}, uint32(cfg.MaximumPageSize))
   442  	if err != nil {
   443  		return nil, fmt.Errorf("LDAP search failed: %w", err)
   444  	}
   445  
   446  	return result.Entries, nil
   447  }
   448  
   449  func sidBytesToString(b []byte) (string, error) {
   450  	reader := bytes.NewReader(b)
   451  
   452  	var revision, subAuthorityCount uint8
   453  	var identifierAuthorityParts [3]uint16
   454  
   455  	if err := binary.Read(reader, binary.LittleEndian, &revision); err != nil {
   456  		return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading Revision: {{err}}", b), err)
   457  	}
   458  
   459  	if err := binary.Read(reader, binary.LittleEndian, &subAuthorityCount); err != nil {
   460  		return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading SubAuthorityCount: {{err}}", b), err)
   461  	}
   462  
   463  	if err := binary.Read(reader, binary.BigEndian, &identifierAuthorityParts); err != nil {
   464  		return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading IdentifierAuthority: {{err}}", b), err)
   465  	}
   466  	identifierAuthority := (uint64(identifierAuthorityParts[0]) << 32) + (uint64(identifierAuthorityParts[1]) << 16) + uint64(identifierAuthorityParts[2])
   467  
   468  	subAuthority := make([]uint32, subAuthorityCount)
   469  	if err := binary.Read(reader, binary.LittleEndian, &subAuthority); err != nil {
   470  		return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading SubAuthority: {{err}}", b), err)
   471  	}
   472  
   473  	result := fmt.Sprintf("S-%d-%d", revision, identifierAuthority)
   474  	for _, subAuthorityPart := range subAuthority {
   475  		result += fmt.Sprintf("-%d", subAuthorityPart)
   476  	}
   477  
   478  	return result, nil
   479  }
   480  
   481  func (c *Client) performLdapTokenGroupsSearch(cfg *ConfigEntry, conn Connection, userDN string) ([]*ldap.Entry, error) {
   482  	var wg sync.WaitGroup
   483  	var lock sync.Mutex
   484  	taskChan := make(chan string)
   485  	maxWorkers := 10
   486  
   487  	result, err := conn.Search(&ldap.SearchRequest{
   488  		BaseDN:       userDN,
   489  		Scope:        ldap.ScopeBaseObject,
   490  		DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   491  		Filter:       "(objectClass=*)",
   492  		Attributes: []string{
   493  			"tokenGroups",
   494  		},
   495  		SizeLimit: 1,
   496  	})
   497  	if err != nil {
   498  		return nil, fmt.Errorf("LDAP search failed: %w", err)
   499  	}
   500  	if len(result.Entries) == 0 {
   501  		c.Logger.Warn("unable to read object for group attributes", "userdn", userDN, "groupattr", cfg.GroupAttr)
   502  		return make([]*ldap.Entry, 0), nil
   503  	}
   504  
   505  	userEntry := result.Entries[0]
   506  	groupAttrValues := userEntry.GetRawAttributeValues("tokenGroups")
   507  	groupEntries := make([]*ldap.Entry, 0, len(groupAttrValues))
   508  
   509  	for i := 0; i < maxWorkers; i++ {
   510  		wg.Add(1)
   511  		go func() {
   512  			defer wg.Done()
   513  
   514  			for sid := range taskChan {
   515  				groupResult, err := conn.Search(&ldap.SearchRequest{
   516  					BaseDN:       fmt.Sprintf("<SID=%s>", sid),
   517  					Scope:        ldap.ScopeBaseObject,
   518  					DerefAliases: ldapDerefAliasMap[cfg.DerefAliases],
   519  					Filter:       "(objectClass=*)",
   520  					Attributes: []string{
   521  						"1.1", // RFC no attributes
   522  					},
   523  					SizeLimit: 1,
   524  				})
   525  				if err != nil {
   526  					c.Logger.Warn("unable to read the group sid", "sid", sid)
   527  					continue
   528  				}
   529  
   530  				if len(groupResult.Entries) == 0 {
   531  					c.Logger.Warn("unable to find the group", "sid", sid)
   532  					continue
   533  				}
   534  
   535  				lock.Lock()
   536  				groupEntries = append(groupEntries, groupResult.Entries[0])
   537  				lock.Unlock()
   538  			}
   539  		}()
   540  	}
   541  
   542  	for _, sidBytes := range groupAttrValues {
   543  		sidString, err := sidBytesToString(sidBytes)
   544  		if err != nil {
   545  			c.Logger.Warn("unable to read sid", "err", err)
   546  			continue
   547  		}
   548  		taskChan <- sidString
   549  	}
   550  
   551  	close(taskChan)
   552  	wg.Wait()
   553  
   554  	return groupEntries, nil
   555  }
   556  
   557  /*
   558   * getLdapGroups queries LDAP and returns a slice describing the set of groups the authenticated user is a member of.
   559   *
   560   * If cfg.UseTokenGroups is true then the search is performed directly on the userDN.
   561   * The values of those attributes are converted to string SIDs, and then looked up to get ldap.Entry objects.
   562   * Otherwise, the search query is constructed according to cfg.GroupFilter, and run in context of cfg.GroupDN.
   563   * Groups will be resolved from the query results by following the attribute defined in cfg.GroupAttr.
   564   *
   565   * cfg.GroupFilter is a go template and is compiled with the following context: [UserDN, Username]
   566   *    UserDN - The DN of the authenticated user
   567   *    Username - The Username of the authenticated user
   568   *
   569   * Example:
   570   *   cfg.GroupFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))"
   571   *   cfg.GroupDN     = "OU=Groups,DC=myorg,DC=com"
   572   *   cfg.GroupAttr   = "cn"
   573   *
   574   * NOTE - If cfg.GroupFilter is empty, no query is performed and an empty result slice is returned.
   575   *
   576   */
   577  func (c *Client) GetLdapGroups(cfg *ConfigEntry, conn Connection, userDN string, username string) ([]string, error) {
   578  	var entries []*ldap.Entry
   579  	var err error
   580  	if cfg.UseTokenGroups {
   581  		entries, err = c.performLdapTokenGroupsSearch(cfg, conn, userDN)
   582  	} else {
   583  		if paging, ok := conn.(PagingConnection); ok && cfg.MaximumPageSize > 0 {
   584  			entries, err = c.performLdapFilterGroupsSearchPaging(cfg, paging, userDN, username)
   585  		} else {
   586  			entries, err = c.performLdapFilterGroupsSearch(cfg, conn, userDN, username)
   587  		}
   588  	}
   589  	if err != nil {
   590  		return nil, err
   591  	}
   592  
   593  	// retrieve the groups in a string/bool map as a structure to avoid duplicates inside
   594  	ldapMap := make(map[string]bool)
   595  
   596  	for _, e := range entries {
   597  		dn, err := ldap.ParseDN(e.DN)
   598  		if err != nil || len(dn.RDNs) == 0 {
   599  			continue
   600  		}
   601  
   602  		// Enumerate attributes of each result, parse out CN and add as group
   603  		values := e.GetAttributeValues(cfg.GroupAttr)
   604  		if len(values) > 0 {
   605  			for _, val := range values {
   606  				groupCN := getCN(cfg, val)
   607  				ldapMap[groupCN] = true
   608  			}
   609  		} else {
   610  			// If groupattr didn't resolve, use self (enumerating group objects)
   611  			groupCN := getCN(cfg, e.DN)
   612  			ldapMap[groupCN] = true
   613  		}
   614  	}
   615  
   616  	ldapGroups := make([]string, 0, len(ldapMap))
   617  	for key := range ldapMap {
   618  		ldapGroups = append(ldapGroups, key)
   619  	}
   620  
   621  	return ldapGroups, nil
   622  }
   623  
   624  // EscapeLDAPValue is exported because a plugin uses it outside this package.
   625  // EscapeLDAPValue will properly escape the input string as an ldap value
   626  // rfc4514 states the following must be escaped:
   627  // - leading space or hash
   628  // - trailing space
   629  // - special characters '"', '+', ',', ';', '<', '>', '\\'
   630  // - hex
   631  func EscapeLDAPValue(input string) string {
   632  	if input == "" {
   633  		return ""
   634  	}
   635  
   636  	buf := bytes.Buffer{}
   637  
   638  	escFn := func(c byte) {
   639  		buf.WriteByte('\\')
   640  		buf.WriteByte(c)
   641  	}
   642  
   643  	inputLen := len(input)
   644  	for i := 0; i < inputLen; i++ {
   645  		char := input[i]
   646  		switch {
   647  		case i == 0 && char == ' ' || char == '#':
   648  			// leading space or hash.
   649  			escFn(char)
   650  			continue
   651  		case i == inputLen-1 && char == ' ':
   652  			// trailing space.
   653  			escFn(char)
   654  			continue
   655  		case specialChar(char):
   656  			escFn(char)
   657  			continue
   658  		case char < ' ' || char > '~':
   659  			// anything that's not between the ascii space and tilde must be hex
   660  			buf.WriteByte('\\')
   661  			buf.WriteString(hex.EncodeToString([]byte{char}))
   662  			continue
   663  		default:
   664  			// everything remaining, doesn't need to be escaped
   665  			buf.WriteByte(char)
   666  		}
   667  	}
   668  	return buf.String()
   669  }
   670  
   671  func specialChar(char byte) bool {
   672  	switch char {
   673  	case '"', '+', ',', ';', '<', '>', '\\':
   674  		return true
   675  	default:
   676  		return false
   677  	}
   678  }
   679  
   680  /*
   681   * Parses a distinguished name and returns the CN portion.
   682   * Given a non-conforming string (such as an already-extracted CN),
   683   * it will be returned as-is.
   684   */
   685  func getCN(cfg *ConfigEntry, dn string) string {
   686  	parsedDN, err := ldap.ParseDN(dn)
   687  	if err != nil || len(parsedDN.RDNs) == 0 {
   688  		// It was already a CN, return as-is
   689  		return dn
   690  	}
   691  
   692  	for _, rdn := range parsedDN.RDNs {
   693  		for _, rdnAttr := range rdn.Attributes {
   694  			if cfg.UsePre111GroupCNBehavior == nil || *cfg.UsePre111GroupCNBehavior {
   695  				if rdnAttr.Type == "CN" {
   696  					return rdnAttr.Value
   697  				}
   698  			} else {
   699  				if strings.EqualFold(rdnAttr.Type, "CN") {
   700  					return rdnAttr.Value
   701  				}
   702  			}
   703  		}
   704  	}
   705  
   706  	// Default, return self
   707  	return dn
   708  }
   709  
   710  func getTLSConfig(cfg *ConfigEntry, host string) (*tls.Config, error) {
   711  	tlsConfig := &tls.Config{
   712  		ServerName: host,
   713  	}
   714  
   715  	if cfg.TLSMinVersion != "" {
   716  		tlsMinVersion, ok := tlsutil.TLSLookup[cfg.TLSMinVersion]
   717  		if !ok {
   718  			return nil, fmt.Errorf("invalid 'tls_min_version' in config")
   719  		}
   720  		tlsConfig.MinVersion = tlsMinVersion
   721  	}
   722  
   723  	if cfg.TLSMaxVersion != "" {
   724  		tlsMaxVersion, ok := tlsutil.TLSLookup[cfg.TLSMaxVersion]
   725  		if !ok {
   726  			return nil, fmt.Errorf("invalid 'tls_max_version' in config")
   727  		}
   728  		tlsConfig.MaxVersion = tlsMaxVersion
   729  	}
   730  
   731  	if cfg.InsecureTLS {
   732  		tlsConfig.InsecureSkipVerify = true
   733  	}
   734  	if cfg.Certificate != "" {
   735  		caPool := x509.NewCertPool()
   736  		ok := caPool.AppendCertsFromPEM([]byte(cfg.Certificate))
   737  		if !ok {
   738  			return nil, fmt.Errorf("could not append CA certificate")
   739  		}
   740  		tlsConfig.RootCAs = caPool
   741  	}
   742  	if cfg.ClientTLSCert != "" && cfg.ClientTLSKey != "" {
   743  		certificate, err := tls.X509KeyPair([]byte(cfg.ClientTLSCert), []byte(cfg.ClientTLSKey))
   744  		if err != nil {
   745  			return nil, fmt.Errorf("failed to parse client X509 key pair: %w", err)
   746  		}
   747  		tlsConfig.Certificates = append(tlsConfig.Certificates, certificate)
   748  	} else if cfg.ClientTLSCert != "" || cfg.ClientTLSKey != "" {
   749  		return nil, fmt.Errorf("both client_tls_cert and client_tls_key must be set")
   750  	}
   751  	return tlsConfig, nil
   752  }