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