github.com/hashicorp/vault/sdk@v0.13.0/helper/ldaputil/config.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package ldaputil
     5  
     6  import (
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"encoding/pem"
    10  	"errors"
    11  	"fmt"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/go-ldap/ldap/v3"
    16  	capldap "github.com/hashicorp/cap/ldap"
    17  	"github.com/hashicorp/errwrap"
    18  	"github.com/hashicorp/go-secure-stdlib/tlsutil"
    19  	"github.com/hashicorp/vault/sdk/framework"
    20  )
    21  
    22  var ldapDerefAliasMap = map[string]int{
    23  	"never":     ldap.NeverDerefAliases,
    24  	"finding":   ldap.DerefFindingBaseObj,
    25  	"searching": ldap.DerefInSearching,
    26  	"always":    ldap.DerefAlways,
    27  }
    28  
    29  // ConfigFields returns all the config fields that can potentially be used by the LDAP client.
    30  // Not all fields will be used by every integration.
    31  func ConfigFields() map[string]*framework.FieldSchema {
    32  	return map[string]*framework.FieldSchema{
    33  		"anonymous_group_search": {
    34  			Type:        framework.TypeBool,
    35  			Default:     false,
    36  			Description: "Use anonymous binds when performing LDAP group searches (if true the initial credentials will still be used for the initial connection test).",
    37  			DisplayAttrs: &framework.DisplayAttributes{
    38  				Name: "Anonymous group search",
    39  			},
    40  		},
    41  		"url": {
    42  			Type:        framework.TypeString,
    43  			Default:     "ldap://127.0.0.1",
    44  			Description: "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order.",
    45  			DisplayAttrs: &framework.DisplayAttributes{
    46  				Name: "URL",
    47  			},
    48  		},
    49  
    50  		"userdn": {
    51  			Type:        framework.TypeString,
    52  			Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)",
    53  			DisplayAttrs: &framework.DisplayAttributes{
    54  				Name: "User DN",
    55  			},
    56  		},
    57  
    58  		"binddn": {
    59  			Type:        framework.TypeString,
    60  			Description: "LDAP DN for searching for the user DN (optional)",
    61  			DisplayAttrs: &framework.DisplayAttributes{
    62  				Name: "Name of Object to bind (binddn)",
    63  			},
    64  		},
    65  
    66  		"bindpass": {
    67  			Type:        framework.TypeString,
    68  			Description: "LDAP password for searching for the user DN (optional)",
    69  			DisplayAttrs: &framework.DisplayAttributes{
    70  				Sensitive: true,
    71  			},
    72  		},
    73  
    74  		"groupdn": {
    75  			Type:        framework.TypeString,
    76  			Description: "LDAP search base to use for group membership search (eg: ou=Groups,dc=example,dc=org)",
    77  			DisplayAttrs: &framework.DisplayAttributes{
    78  				Name: "Group DN",
    79  			},
    80  		},
    81  
    82  		"groupfilter": {
    83  			Type:    framework.TypeString,
    84  			Default: "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))",
    85  			Description: `Go template for querying group membership of user (optional)
    86  The template can access the following context variables: UserDN, Username
    87  Example: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))
    88  Default: (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))`,
    89  			DisplayAttrs: &framework.DisplayAttributes{
    90  				Name: "Group Filter",
    91  			},
    92  		},
    93  
    94  		"groupattr": {
    95  			Type:    framework.TypeString,
    96  			Default: "cn",
    97  			Description: `LDAP attribute to follow on objects returned by <groupfilter>
    98  in order to enumerate user group membership.
    99  Examples: "cn" or "memberOf", etc.
   100  Default: cn`,
   101  			DisplayAttrs: &framework.DisplayAttributes{
   102  				Name:  "Group Attribute",
   103  				Value: "cn",
   104  			},
   105  		},
   106  
   107  		"userfilter": {
   108  			Type:    framework.TypeString,
   109  			Default: "({{.UserAttr}}={{.Username}})",
   110  			Description: `Go template for LDAP user search filer (optional)
   111  The template can access the following context variables: UserAttr, Username
   112  Default: ({{.UserAttr}}={{.Username}})`,
   113  			DisplayAttrs: &framework.DisplayAttributes{
   114  				Name: "User Search Filter",
   115  			},
   116  		},
   117  
   118  		"upndomain": {
   119  			Type:        framework.TypeString,
   120  			Description: "Enables userPrincipalDomain login with [username]@UPNDomain (optional)",
   121  			DisplayAttrs: &framework.DisplayAttributes{
   122  				Name: "User Principal (UPN) Domain",
   123  			},
   124  		},
   125  
   126  		"username_as_alias": {
   127  			Type:        framework.TypeBool,
   128  			Default:     false,
   129  			Description: "If true, sets the alias name to the username",
   130  		},
   131  
   132  		"userattr": {
   133  			Type:        framework.TypeString,
   134  			Default:     "cn",
   135  			Description: "Attribute used for users (default: cn)",
   136  			DisplayAttrs: &framework.DisplayAttributes{
   137  				Name:  "User Attribute",
   138  				Value: "cn",
   139  			},
   140  		},
   141  
   142  		"certificate": {
   143  			Type:        framework.TypeString,
   144  			Description: "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)",
   145  			DisplayAttrs: &framework.DisplayAttributes{
   146  				Name:     "CA certificate",
   147  				EditType: "file",
   148  			},
   149  		},
   150  
   151  		"client_tls_cert": {
   152  			Type:        framework.TypeString,
   153  			Description: "Client certificate to provide to the LDAP server, must be x509 PEM encoded (optional)",
   154  			DisplayAttrs: &framework.DisplayAttributes{
   155  				Name:     "Client certificate",
   156  				EditType: "file",
   157  			},
   158  		},
   159  
   160  		"client_tls_key": {
   161  			Type:        framework.TypeString,
   162  			Description: "Client certificate key to provide to the LDAP server, must be x509 PEM encoded (optional)",
   163  			DisplayAttrs: &framework.DisplayAttributes{
   164  				Name:     "Client key",
   165  				EditType: "file",
   166  			},
   167  		},
   168  
   169  		"discoverdn": {
   170  			Type:        framework.TypeBool,
   171  			Description: "Use anonymous bind to discover the bind DN of a user (optional)",
   172  			DisplayAttrs: &framework.DisplayAttributes{
   173  				Name: "Discover DN",
   174  			},
   175  		},
   176  
   177  		"insecure_tls": {
   178  			Type:        framework.TypeBool,
   179  			Description: "Skip LDAP server SSL Certificate verification - VERY insecure (optional)",
   180  			DisplayAttrs: &framework.DisplayAttributes{
   181  				Name: "Insecure TLS",
   182  			},
   183  		},
   184  
   185  		"starttls": {
   186  			Type:        framework.TypeBool,
   187  			Description: "Issue a StartTLS command after establishing unencrypted connection (optional)",
   188  			DisplayAttrs: &framework.DisplayAttributes{
   189  				Name: "Issue StartTLS",
   190  			},
   191  		},
   192  
   193  		"tls_min_version": {
   194  			Type:        framework.TypeString,
   195  			Default:     "tls12",
   196  			Description: "Minimum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'. Defaults to 'tls12'",
   197  			DisplayAttrs: &framework.DisplayAttributes{
   198  				Name: "Minimum TLS Version",
   199  			},
   200  			AllowedValues: []interface{}{"tls10", "tls11", "tls12", "tls13"},
   201  		},
   202  
   203  		"tls_max_version": {
   204  			Type:        framework.TypeString,
   205  			Default:     "tls12",
   206  			Description: "Maximum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'. Defaults to 'tls12'",
   207  			DisplayAttrs: &framework.DisplayAttributes{
   208  				Name: "Maximum TLS Version",
   209  			},
   210  			AllowedValues: []interface{}{"tls10", "tls11", "tls12", "tls13"},
   211  		},
   212  
   213  		"deny_null_bind": {
   214  			Type:        framework.TypeBool,
   215  			Default:     true,
   216  			Description: "Denies an unauthenticated LDAP bind request if the user's password is empty; defaults to true",
   217  		},
   218  
   219  		"case_sensitive_names": {
   220  			Type:        framework.TypeBool,
   221  			Description: "If true, case sensitivity will be used when comparing usernames and groups for matching policies.",
   222  		},
   223  
   224  		"use_token_groups": {
   225  			Type:        framework.TypeBool,
   226  			Default:     false,
   227  			Description: "If true, use the Active Directory tokenGroups constructed attribute of the user to find the group memberships. This will find all security groups including nested ones.",
   228  		},
   229  
   230  		"use_pre111_group_cn_behavior": {
   231  			Type:        framework.TypeBool,
   232  			Description: "In Vault 1.1.1 a fix for handling group CN values of different cases unfortunately introduced a regression that could cause previously defined groups to not be found due to a change in the resulting name. If set true, the pre-1.1.1 behavior for matching group CNs will be used. This is only needed in some upgrade scenarios for backwards compatibility. It is enabled by default if the config is upgraded but disabled by default on new configurations.",
   233  		},
   234  
   235  		"request_timeout": {
   236  			Type:        framework.TypeDurationSecond,
   237  			Description: "Timeout, in seconds, for the connection when making requests against the server before returning back an error.",
   238  			Default:     "90s",
   239  		},
   240  
   241  		"connection_timeout": {
   242  			Type:        framework.TypeDurationSecond,
   243  			Description: "Timeout, in seconds, when attempting to connect to the LDAP server before trying the next URL in the configuration.",
   244  			Default:     "30s",
   245  		},
   246  
   247  		"dereference_aliases": {
   248  			Type:          framework.TypeString,
   249  			Description:   "When aliases should be dereferenced on search operations. Accepted values are 'never', 'finding', 'searching', 'always'. Defaults to 'never'.",
   250  			Default:       "never",
   251  			AllowedValues: []interface{}{"never", "finding", "searching", "always"},
   252  		},
   253  
   254  		"max_page_size": {
   255  			Type:        framework.TypeInt,
   256  			Description: "If set to a value greater than 0, the LDAP backend will use the LDAP server's paged search control to request pages of up to the given size. This can be used to avoid hitting the LDAP server's maximum result size limit. Otherwise, the LDAP backend will not use the paged search control.",
   257  			Default:     0,
   258  		},
   259  	}
   260  }
   261  
   262  /*
   263   * Creates and initializes a ConfigEntry object with its default values,
   264   * as specified by the passed schema.
   265   */
   266  func NewConfigEntry(existing *ConfigEntry, d *framework.FieldData) (*ConfigEntry, error) {
   267  	var hadExisting bool
   268  	var cfg *ConfigEntry
   269  
   270  	if existing != nil {
   271  		cfg = existing
   272  		hadExisting = true
   273  	} else {
   274  		cfg = new(ConfigEntry)
   275  	}
   276  
   277  	if _, ok := d.Raw["anonymous_group_search"]; ok || !hadExisting {
   278  		cfg.AnonymousGroupSearch = d.Get("anonymous_group_search").(bool)
   279  	}
   280  
   281  	if _, ok := d.Raw["username_as_alias"]; ok || !hadExisting {
   282  		cfg.UsernameAsAlias = d.Get("username_as_alias").(bool)
   283  	}
   284  
   285  	if _, ok := d.Raw["url"]; ok || !hadExisting {
   286  		cfg.Url = strings.ToLower(d.Get("url").(string))
   287  	}
   288  
   289  	if _, ok := d.Raw["userfilter"]; ok || !hadExisting {
   290  		userfilter := d.Get("userfilter").(string)
   291  		if userfilter != "" {
   292  			// Validate the template before proceeding
   293  			_, err := template.New("queryTemplate").Parse(userfilter)
   294  			if err != nil {
   295  				return nil, errwrap.Wrapf("invalid userfilter: {{err}}", err)
   296  			}
   297  		}
   298  
   299  		cfg.UserFilter = userfilter
   300  	}
   301  
   302  	if _, ok := d.Raw["userattr"]; ok || !hadExisting {
   303  		cfg.UserAttr = strings.ToLower(d.Get("userattr").(string))
   304  	}
   305  
   306  	if _, ok := d.Raw["userdn"]; ok || !hadExisting {
   307  		cfg.UserDN = d.Get("userdn").(string)
   308  	}
   309  
   310  	if _, ok := d.Raw["groupdn"]; ok || !hadExisting {
   311  		cfg.GroupDN = d.Get("groupdn").(string)
   312  	}
   313  
   314  	if _, ok := d.Raw["groupfilter"]; ok || !hadExisting {
   315  		groupfilter := d.Get("groupfilter").(string)
   316  		if groupfilter != "" {
   317  			// Validate the template before proceeding
   318  			_, err := template.New("queryTemplate").Parse(groupfilter)
   319  			if err != nil {
   320  				return nil, errwrap.Wrapf("invalid groupfilter: {{err}}", err)
   321  			}
   322  		}
   323  
   324  		cfg.GroupFilter = groupfilter
   325  	}
   326  
   327  	if _, ok := d.Raw["groupattr"]; ok || !hadExisting {
   328  		cfg.GroupAttr = d.Get("groupattr").(string)
   329  	}
   330  
   331  	if _, ok := d.Raw["upndomain"]; ok || !hadExisting {
   332  		cfg.UPNDomain = d.Get("upndomain").(string)
   333  	}
   334  
   335  	if _, ok := d.Raw["certificate"]; ok || !hadExisting {
   336  		certificate := d.Get("certificate").(string)
   337  		if certificate != "" {
   338  			if err := validateCertificate([]byte(certificate)); err != nil {
   339  				return nil, errwrap.Wrapf("failed to parse server tls cert: {{err}}", err)
   340  			}
   341  		}
   342  		cfg.Certificate = certificate
   343  	}
   344  
   345  	if _, ok := d.Raw["client_tls_cert"]; ok || !hadExisting {
   346  		clientTLSCert := d.Get("client_tls_cert").(string)
   347  		cfg.ClientTLSCert = clientTLSCert
   348  	}
   349  
   350  	if _, ok := d.Raw["client_tls_key"]; ok || !hadExisting {
   351  		clientTLSKey := d.Get("client_tls_key").(string)
   352  		cfg.ClientTLSKey = clientTLSKey
   353  	}
   354  
   355  	if cfg.ClientTLSCert != "" && cfg.ClientTLSKey != "" {
   356  		if _, err := tls.X509KeyPair([]byte(cfg.ClientTLSCert), []byte(cfg.ClientTLSKey)); err != nil {
   357  			return nil, errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err)
   358  		}
   359  	} else if cfg.ClientTLSCert != "" || cfg.ClientTLSKey != "" {
   360  		return nil, fmt.Errorf("both client_tls_cert and client_tls_key must be set")
   361  	}
   362  
   363  	if _, ok := d.Raw["insecure_tls"]; ok || !hadExisting {
   364  		cfg.InsecureTLS = d.Get("insecure_tls").(bool)
   365  	}
   366  
   367  	if _, ok := d.Raw["tls_min_version"]; ok || !hadExisting {
   368  		cfg.TLSMinVersion = d.Get("tls_min_version").(string)
   369  		_, ok = tlsutil.TLSLookup[cfg.TLSMinVersion]
   370  		if !ok {
   371  			return nil, errors.New("invalid 'tls_min_version'")
   372  		}
   373  	}
   374  
   375  	if _, ok := d.Raw["tls_max_version"]; ok || !hadExisting {
   376  		cfg.TLSMaxVersion = d.Get("tls_max_version").(string)
   377  		_, ok = tlsutil.TLSLookup[cfg.TLSMaxVersion]
   378  		if !ok {
   379  			return nil, fmt.Errorf("invalid 'tls_max_version'")
   380  		}
   381  	}
   382  	if cfg.TLSMaxVersion < cfg.TLSMinVersion {
   383  		return nil, fmt.Errorf("'tls_max_version' must be greater than or equal to 'tls_min_version'")
   384  	}
   385  
   386  	if _, ok := d.Raw["starttls"]; ok || !hadExisting {
   387  		cfg.StartTLS = d.Get("starttls").(bool)
   388  	}
   389  
   390  	if _, ok := d.Raw["binddn"]; ok || !hadExisting {
   391  		cfg.BindDN = d.Get("binddn").(string)
   392  	}
   393  
   394  	if _, ok := d.Raw["bindpass"]; ok || !hadExisting {
   395  		cfg.BindPassword = d.Get("bindpass").(string)
   396  	}
   397  
   398  	if _, ok := d.Raw["deny_null_bind"]; ok || !hadExisting {
   399  		cfg.DenyNullBind = d.Get("deny_null_bind").(bool)
   400  	}
   401  
   402  	if _, ok := d.Raw["discoverdn"]; ok || !hadExisting {
   403  		cfg.DiscoverDN = d.Get("discoverdn").(bool)
   404  	}
   405  
   406  	if _, ok := d.Raw["case_sensitive_names"]; ok || !hadExisting {
   407  		cfg.CaseSensitiveNames = new(bool)
   408  		*cfg.CaseSensitiveNames = d.Get("case_sensitive_names").(bool)
   409  	}
   410  
   411  	usePre111GroupCNBehavior, ok := d.GetOk("use_pre111_group_cn_behavior")
   412  	if ok {
   413  		cfg.UsePre111GroupCNBehavior = new(bool)
   414  		*cfg.UsePre111GroupCNBehavior = usePre111GroupCNBehavior.(bool)
   415  	}
   416  
   417  	if _, ok := d.Raw["use_token_groups"]; ok || !hadExisting {
   418  		cfg.UseTokenGroups = d.Get("use_token_groups").(bool)
   419  	}
   420  
   421  	if _, ok := d.Raw["request_timeout"]; ok || !hadExisting {
   422  		cfg.RequestTimeout = d.Get("request_timeout").(int)
   423  	}
   424  
   425  	if _, ok := d.Raw["connection_timeout"]; ok || !hadExisting {
   426  		cfg.ConnectionTimeout = d.Get("connection_timeout").(int)
   427  	}
   428  
   429  	if _, ok := d.Raw["dereference_aliases"]; ok || !hadExisting {
   430  		cfg.DerefAliases = d.Get("dereference_aliases").(string)
   431  	}
   432  
   433  	if _, ok := d.Raw["max_page_size"]; ok || !hadExisting {
   434  		cfg.MaximumPageSize = d.Get("max_page_size").(int)
   435  	}
   436  
   437  	return cfg, nil
   438  }
   439  
   440  type ConfigEntry struct {
   441  	Url                      string `json:"url"`
   442  	UserDN                   string `json:"userdn"`
   443  	AnonymousGroupSearch     bool   `json:"anonymous_group_search"`
   444  	GroupDN                  string `json:"groupdn"`
   445  	GroupFilter              string `json:"groupfilter"`
   446  	GroupAttr                string `json:"groupattr"`
   447  	UPNDomain                string `json:"upndomain"`
   448  	UsernameAsAlias          bool   `json:"username_as_alias"`
   449  	UserFilter               string `json:"userfilter"`
   450  	UserAttr                 string `json:"userattr"`
   451  	Certificate              string `json:"certificate"`
   452  	InsecureTLS              bool   `json:"insecure_tls"`
   453  	StartTLS                 bool   `json:"starttls"`
   454  	BindDN                   string `json:"binddn"`
   455  	BindPassword             string `json:"bindpass"`
   456  	DenyNullBind             bool   `json:"deny_null_bind"`
   457  	DiscoverDN               bool   `json:"discoverdn"`
   458  	TLSMinVersion            string `json:"tls_min_version"`
   459  	TLSMaxVersion            string `json:"tls_max_version"`
   460  	UseTokenGroups           bool   `json:"use_token_groups"`
   461  	UsePre111GroupCNBehavior *bool  `json:"use_pre111_group_cn_behavior"`
   462  	RequestTimeout           int    `json:"request_timeout"`
   463  	ConnectionTimeout        int    `json:"connection_timeout"` // deprecated: use RequestTimeout
   464  	DerefAliases             string `json:"dereference_aliases"`
   465  	MaximumPageSize          int    `json:"max_page_size"`
   466  
   467  	// These json tags deviate from snake case because there was a past issue
   468  	// where the tag was being ignored, causing it to be jsonified as "CaseSensitiveNames", etc.
   469  	// To continue reading in users' previously stored values,
   470  	// we chose to carry that forward.
   471  	CaseSensitiveNames *bool  `json:"CaseSensitiveNames,omitempty"`
   472  	ClientTLSCert      string `json:"ClientTLSCert"`
   473  	ClientTLSKey       string `json:"ClientTLSKey"`
   474  }
   475  
   476  func (c *ConfigEntry) Map() map[string]interface{} {
   477  	m := c.PasswordlessMap()
   478  	m["bindpass"] = c.BindPassword
   479  	return m
   480  }
   481  
   482  func (c *ConfigEntry) PasswordlessMap() map[string]interface{} {
   483  	m := map[string]interface{}{
   484  		"url":                    c.Url,
   485  		"userdn":                 c.UserDN,
   486  		"groupdn":                c.GroupDN,
   487  		"groupfilter":            c.GroupFilter,
   488  		"groupattr":              c.GroupAttr,
   489  		"userfilter":             c.UserFilter,
   490  		"upndomain":              c.UPNDomain,
   491  		"userattr":               c.UserAttr,
   492  		"certificate":            c.Certificate,
   493  		"insecure_tls":           c.InsecureTLS,
   494  		"starttls":               c.StartTLS,
   495  		"binddn":                 c.BindDN,
   496  		"deny_null_bind":         c.DenyNullBind,
   497  		"discoverdn":             c.DiscoverDN,
   498  		"tls_min_version":        c.TLSMinVersion,
   499  		"tls_max_version":        c.TLSMaxVersion,
   500  		"use_token_groups":       c.UseTokenGroups,
   501  		"anonymous_group_search": c.AnonymousGroupSearch,
   502  		"request_timeout":        c.RequestTimeout,
   503  		"connection_timeout":     c.ConnectionTimeout,
   504  		"username_as_alias":      c.UsernameAsAlias,
   505  		"dereference_aliases":    c.DerefAliases,
   506  		"max_page_size":          c.MaximumPageSize,
   507  	}
   508  	if c.CaseSensitiveNames != nil {
   509  		m["case_sensitive_names"] = *c.CaseSensitiveNames
   510  	}
   511  	if c.UsePre111GroupCNBehavior != nil {
   512  		m["use_pre111_group_cn_behavior"] = *c.UsePre111GroupCNBehavior
   513  	}
   514  	return m
   515  }
   516  
   517  func validateCertificate(pemBlock []byte) error {
   518  	block, _ := pem.Decode([]byte(pemBlock))
   519  	if block == nil || block.Type != "CERTIFICATE" {
   520  		return errors.New("failed to decode PEM block in the certificate")
   521  	}
   522  	_, err := x509.ParseCertificate(block.Bytes)
   523  	if err != nil {
   524  		return fmt.Errorf("failed to parse certificate %s", err.Error())
   525  	}
   526  	return nil
   527  }
   528  
   529  func (c *ConfigEntry) Validate() error {
   530  	if len(c.Url) == 0 {
   531  		return errors.New("at least one url must be provided")
   532  	}
   533  	// Note: This logic is driven by the logic in GetUserBindDN.
   534  	// If updating this, please also update the logic there.
   535  	if !c.DiscoverDN && (c.BindDN == "" || c.BindPassword == "") && c.UPNDomain == "" && c.UserDN == "" {
   536  		return errors.New("cannot derive UserBindDN")
   537  	}
   538  	tlsMinVersion, ok := tlsutil.TLSLookup[c.TLSMinVersion]
   539  	if !ok {
   540  		return errors.New("invalid 'tls_min_version' in config")
   541  	}
   542  	tlsMaxVersion, ok := tlsutil.TLSLookup[c.TLSMaxVersion]
   543  	if !ok {
   544  		return errors.New("invalid 'tls_max_version' in config")
   545  	}
   546  	if tlsMaxVersion < tlsMinVersion {
   547  		return errors.New("'tls_max_version' must be greater than or equal to 'tls_min_version'")
   548  	}
   549  	if c.Certificate != "" {
   550  		if err := validateCertificate([]byte(c.Certificate)); err != nil {
   551  			return errwrap.Wrapf("failed to parse server tls cert: {{err}}", err)
   552  		}
   553  	}
   554  	if c.ClientTLSCert != "" && c.ClientTLSKey != "" {
   555  		if _, err := tls.X509KeyPair([]byte(c.ClientTLSCert), []byte(c.ClientTLSKey)); err != nil {
   556  			return errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err)
   557  		}
   558  	}
   559  	return nil
   560  }
   561  
   562  func ConvertConfig(cfg *ConfigEntry) *capldap.ClientConfig {
   563  	// cap/ldap doesn't have a notion of connection_timeout, and uses a single timeout value for
   564  	// both the net.Dialer and ldap connection timeout.
   565  	// So take the smaller of the two values and use that as the timeout value.
   566  	minTimeout := min(cfg.ConnectionTimeout, cfg.RequestTimeout)
   567  	urls := strings.Split(cfg.Url, ",")
   568  	config := &capldap.ClientConfig{
   569  		URLs:                                 urls,
   570  		UserDN:                               cfg.UserDN,
   571  		AnonymousGroupSearch:                 cfg.AnonymousGroupSearch,
   572  		GroupDN:                              cfg.GroupDN,
   573  		GroupFilter:                          cfg.GroupFilter,
   574  		GroupAttr:                            cfg.GroupAttr,
   575  		UPNDomain:                            cfg.UPNDomain,
   576  		UserFilter:                           cfg.UserFilter,
   577  		UserAttr:                             cfg.UserAttr,
   578  		ClientTLSCert:                        cfg.ClientTLSCert,
   579  		ClientTLSKey:                         cfg.ClientTLSKey,
   580  		InsecureTLS:                          cfg.InsecureTLS,
   581  		StartTLS:                             cfg.StartTLS,
   582  		BindDN:                               cfg.BindDN,
   583  		BindPassword:                         cfg.BindPassword,
   584  		AllowEmptyPasswordBinds:              !cfg.DenyNullBind,
   585  		DiscoverDN:                           cfg.DiscoverDN,
   586  		TLSMinVersion:                        cfg.TLSMinVersion,
   587  		TLSMaxVersion:                        cfg.TLSMaxVersion,
   588  		UseTokenGroups:                       cfg.UseTokenGroups,
   589  		RequestTimeout:                       minTimeout,
   590  		IncludeUserAttributes:                true,
   591  		ExcludedUserAttributes:               nil,
   592  		IncludeUserGroups:                    true,
   593  		LowerUserAttributeKeys:               true,
   594  		AllowEmptyAnonymousGroupSearch:       true,
   595  		MaximumPageSize:                      cfg.MaximumPageSize,
   596  		DerefAliases:                         cfg.DerefAliases,
   597  		DeprecatedVaultPre111GroupCNBehavior: cfg.UsePre111GroupCNBehavior,
   598  	}
   599  
   600  	if cfg.Certificate != "" {
   601  		config.Certificates = []string{cfg.Certificate}
   602  	}
   603  
   604  	return config
   605  }
   606  
   607  func min(a, b int) int {
   608  	if a < b {
   609  		return a
   610  	}
   611  	return b
   612  }