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 }