1// Copyright 2014 The Gogs Authors. All rights reserved. 2// Copyright 2020 The Gitea Authors. All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package ldap 7 8import ( 9 "crypto/tls" 10 "fmt" 11 "net" 12 "strconv" 13 "strings" 14 15 "code.gitea.io/gitea/modules/log" 16 17 "github.com/go-ldap/ldap/v3" 18) 19 20// SearchResult : user data 21type SearchResult struct { 22 Username string // Username 23 Name string // Name 24 Surname string // Surname 25 Mail string // E-mail address 26 SSHPublicKey []string // SSH Public Key 27 IsAdmin bool // if user is administrator 28 IsRestricted bool // if user is restricted 29 LowerName string // Lowername 30 Avatar []byte 31} 32 33func (ls *Source) sanitizedUserQuery(username string) (string, bool) { 34 // See http://tools.ietf.org/search/rfc4515 35 badCharacters := "\x00()*\\" 36 if strings.ContainsAny(username, badCharacters) { 37 log.Debug("'%s' contains invalid query characters. Aborting.", username) 38 return "", false 39 } 40 41 return fmt.Sprintf(ls.Filter, username), true 42} 43 44func (ls *Source) sanitizedUserDN(username string) (string, bool) { 45 // See http://tools.ietf.org/search/rfc4514: "special characters" 46 badCharacters := "\x00()*\\,='\"#+;<>" 47 if strings.ContainsAny(username, badCharacters) { 48 log.Debug("'%s' contains invalid DN characters. Aborting.", username) 49 return "", false 50 } 51 52 return fmt.Sprintf(ls.UserDN, username), true 53} 54 55func (ls *Source) sanitizedGroupFilter(group string) (string, bool) { 56 // See http://tools.ietf.org/search/rfc4515 57 badCharacters := "\x00*\\" 58 if strings.ContainsAny(group, badCharacters) { 59 log.Trace("Group filter invalid query characters: %s", group) 60 return "", false 61 } 62 63 return group, true 64} 65 66func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) { 67 // See http://tools.ietf.org/search/rfc4514: "special characters" 68 badCharacters := "\x00()*\\'\"#+;<>" 69 if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") { 70 log.Trace("Group DN contains invalid query characters: %s", groupDn) 71 return "", false 72 } 73 74 return groupDn, true 75} 76 77func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) { 78 log.Trace("Search for LDAP user: %s", name) 79 80 // A search for the user. 81 userFilter, ok := ls.sanitizedUserQuery(name) 82 if !ok { 83 return "", false 84 } 85 86 log.Trace("Searching for DN using filter %s and base %s", userFilter, ls.UserBase) 87 search := ldap.NewSearchRequest( 88 ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, 89 false, userFilter, []string{}, nil) 90 91 // Ensure we found a user 92 sr, err := l.Search(search) 93 if err != nil || len(sr.Entries) < 1 { 94 log.Debug("Failed search using filter[%s]: %v", userFilter, err) 95 return "", false 96 } else if len(sr.Entries) > 1 { 97 log.Debug("Filter '%s' returned more than one user.", userFilter) 98 return "", false 99 } 100 101 userDN := sr.Entries[0].DN 102 if userDN == "" { 103 log.Error("LDAP search was successful, but found no DN!") 104 return "", false 105 } 106 107 return userDN, true 108} 109 110func dial(source *Source) (*ldap.Conn, error) { 111 log.Trace("Dialing LDAP with security protocol (%v) without verifying: %v", source.SecurityProtocol, source.SkipVerify) 112 113 tlsConfig := &tls.Config{ 114 ServerName: source.Host, 115 InsecureSkipVerify: source.SkipVerify, 116 } 117 118 if source.SecurityProtocol == SecurityProtocolLDAPS { 119 return ldap.DialTLS("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)), tlsConfig) 120 } 121 122 conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) 123 if err != nil { 124 return nil, fmt.Errorf("error during Dial: %v", err) 125 } 126 127 if source.SecurityProtocol == SecurityProtocolStartTLS { 128 if err = conn.StartTLS(tlsConfig); err != nil { 129 conn.Close() 130 return nil, fmt.Errorf("error during StartTLS: %v", err) 131 } 132 } 133 134 return conn, nil 135} 136 137func bindUser(l *ldap.Conn, userDN, passwd string) error { 138 log.Trace("Binding with userDN: %s", userDN) 139 err := l.Bind(userDN, passwd) 140 if err != nil { 141 log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err) 142 return err 143 } 144 log.Trace("Bound successfully with userDN: %s", userDN) 145 return err 146} 147 148func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool { 149 if len(ls.AdminFilter) == 0 { 150 return false 151 } 152 log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN) 153 search := ldap.NewSearchRequest( 154 userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter, 155 []string{ls.AttributeName}, 156 nil) 157 158 sr, err := l.Search(search) 159 160 if err != nil { 161 log.Error("LDAP Admin Search with filter %s for %s failed unexpectedly! (%v)", ls.AdminFilter, userDN, err) 162 } else if len(sr.Entries) < 1 { 163 log.Trace("LDAP Admin Search found no matching entries.") 164 } else { 165 return true 166 } 167 return false 168} 169 170func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { 171 if len(ls.RestrictedFilter) == 0 { 172 return false 173 } 174 if ls.RestrictedFilter == "*" { 175 return true 176 } 177 log.Trace("Checking restricted with filter %s and base %s", ls.RestrictedFilter, userDN) 178 search := ldap.NewSearchRequest( 179 userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.RestrictedFilter, 180 []string{ls.AttributeName}, 181 nil) 182 183 sr, err := l.Search(search) 184 185 if err != nil { 186 log.Error("LDAP Restrictred Search with filter %s for %s failed unexpectedly! (%v)", ls.RestrictedFilter, userDN, err) 187 } else if len(sr.Entries) < 1 { 188 log.Trace("LDAP Restricted Search found no matching entries.") 189 } else { 190 return true 191 } 192 return false 193} 194 195// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter 196func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult { 197 // See https://tools.ietf.org/search/rfc4513#section-5.1.2 198 if len(passwd) == 0 { 199 log.Debug("Auth. failed for %s, password cannot be empty", name) 200 return nil 201 } 202 l, err := dial(ls) 203 if err != nil { 204 log.Error("LDAP Connect error, %s:%v", ls.Host, err) 205 ls.Enabled = false 206 return nil 207 } 208 defer l.Close() 209 210 var userDN string 211 if directBind { 212 log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) 213 214 var ok bool 215 userDN, ok = ls.sanitizedUserDN(name) 216 217 if !ok { 218 return nil 219 } 220 221 err = bindUser(l, userDN, passwd) 222 if err != nil { 223 return nil 224 } 225 226 if ls.UserBase != "" { 227 // not everyone has a CN compatible with input name so we need to find 228 // the real userDN in that case 229 230 userDN, ok = ls.findUserDN(l, name) 231 if !ok { 232 return nil 233 } 234 } 235 } else { 236 log.Trace("LDAP will use BindDN.") 237 238 var found bool 239 240 if ls.BindDN != "" && ls.BindPassword != "" { 241 err := l.Bind(ls.BindDN, ls.BindPassword) 242 if err != nil { 243 log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err) 244 return nil 245 } 246 log.Trace("Bound as BindDN %s", ls.BindDN) 247 } else { 248 log.Trace("Proceeding with anonymous LDAP search.") 249 } 250 251 userDN, found = ls.findUserDN(l, name) 252 if !found { 253 return nil 254 } 255 } 256 257 if !ls.AttributesInBind { 258 // binds user (checking password) before looking-up attributes in user context 259 err = bindUser(l, userDN, passwd) 260 if err != nil { 261 return nil 262 } 263 } 264 265 userFilter, ok := ls.sanitizedUserQuery(name) 266 if !ok { 267 return nil 268 } 269 270 isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 271 isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0 272 273 attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail} 274 if len(strings.TrimSpace(ls.UserUID)) > 0 { 275 attribs = append(attribs, ls.UserUID) 276 } 277 if isAttributeSSHPublicKeySet { 278 attribs = append(attribs, ls.AttributeSSHPublicKey) 279 } 280 if isAtributeAvatarSet { 281 attribs = append(attribs, ls.AttributeAvatar) 282 } 283 284 log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, ls.UserUID, userFilter, userDN) 285 search := ldap.NewSearchRequest( 286 userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, 287 attribs, nil) 288 289 sr, err := l.Search(search) 290 if err != nil { 291 log.Error("LDAP Search failed unexpectedly! (%v)", err) 292 return nil 293 } else if len(sr.Entries) < 1 { 294 if directBind { 295 log.Trace("User filter inhibited user login.") 296 } else { 297 log.Trace("LDAP Search found no matching entries.") 298 } 299 300 return nil 301 } 302 303 var sshPublicKey []string 304 var Avatar []byte 305 306 username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername) 307 firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName) 308 surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) 309 mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) 310 uid := sr.Entries[0].GetAttributeValue(ls.UserUID) 311 312 // Check group membership 313 if ls.GroupsEnabled { 314 groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter) 315 if !ok { 316 return nil 317 } 318 groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN) 319 if !ok { 320 return nil 321 } 322 323 log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN) 324 groupSearch := ldap.NewSearchRequest( 325 groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter, 326 []string{ls.GroupMemberUID}, 327 nil) 328 329 srg, err := l.Search(groupSearch) 330 if err != nil { 331 log.Error("LDAP group search failed: %v", err) 332 return nil 333 } else if len(srg.Entries) < 1 { 334 log.Error("LDAP group search failed: 0 entries") 335 return nil 336 } 337 338 isMember := false 339 Entries: 340 for _, group := range srg.Entries { 341 for _, member := range group.GetAttributeValues(ls.GroupMemberUID) { 342 if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid { 343 isMember = true 344 break Entries 345 } 346 } 347 } 348 349 if !isMember { 350 log.Error("LDAP group membership test failed") 351 return nil 352 } 353 } 354 355 if isAttributeSSHPublicKeySet { 356 sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey) 357 } 358 isAdmin := checkAdmin(l, ls, userDN) 359 var isRestricted bool 360 if !isAdmin { 361 isRestricted = checkRestricted(l, ls, userDN) 362 } 363 364 if !directBind && ls.AttributesInBind { 365 // binds user (checking password) after looking-up attributes in BindDN context 366 err = bindUser(l, userDN, passwd) 367 if err != nil { 368 return nil 369 } 370 } 371 372 if isAtributeAvatarSet { 373 Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar) 374 } 375 376 return &SearchResult{ 377 LowerName: strings.ToLower(username), 378 Username: username, 379 Name: firstname, 380 Surname: surname, 381 Mail: mail, 382 SSHPublicKey: sshPublicKey, 383 IsAdmin: isAdmin, 384 IsRestricted: isRestricted, 385 Avatar: Avatar, 386 } 387} 388 389// UsePagedSearch returns if need to use paged search 390func (ls *Source) UsePagedSearch() bool { 391 return ls.SearchPageSize > 0 392} 393 394// SearchEntries : search an LDAP source for all users matching userFilter 395func (ls *Source) SearchEntries() ([]*SearchResult, error) { 396 l, err := dial(ls) 397 if err != nil { 398 log.Error("LDAP Connect error, %s:%v", ls.Host, err) 399 ls.Enabled = false 400 return nil, err 401 } 402 defer l.Close() 403 404 if ls.BindDN != "" && ls.BindPassword != "" { 405 err := l.Bind(ls.BindDN, ls.BindPassword) 406 if err != nil { 407 log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err) 408 return nil, err 409 } 410 log.Trace("Bound as BindDN %s", ls.BindDN) 411 } else { 412 log.Trace("Proceeding with anonymous LDAP search.") 413 } 414 415 userFilter := fmt.Sprintf(ls.Filter, "*") 416 417 isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 418 isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0 419 420 attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail} 421 if isAttributeSSHPublicKeySet { 422 attribs = append(attribs, ls.AttributeSSHPublicKey) 423 } 424 if isAtributeAvatarSet { 425 attribs = append(attribs, ls.AttributeAvatar) 426 } 427 428 log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, userFilter, ls.UserBase) 429 search := ldap.NewSearchRequest( 430 ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, 431 attribs, nil) 432 433 var sr *ldap.SearchResult 434 if ls.UsePagedSearch() { 435 sr, err = l.SearchWithPaging(search, ls.SearchPageSize) 436 } else { 437 sr, err = l.Search(search) 438 } 439 if err != nil { 440 log.Error("LDAP Search failed unexpectedly! (%v)", err) 441 return nil, err 442 } 443 444 result := make([]*SearchResult, len(sr.Entries)) 445 446 for i, v := range sr.Entries { 447 result[i] = &SearchResult{ 448 Username: v.GetAttributeValue(ls.AttributeUsername), 449 Name: v.GetAttributeValue(ls.AttributeName), 450 Surname: v.GetAttributeValue(ls.AttributeSurname), 451 Mail: v.GetAttributeValue(ls.AttributeMail), 452 IsAdmin: checkAdmin(l, ls, v.DN), 453 } 454 if !result[i].IsAdmin { 455 result[i].IsRestricted = checkRestricted(l, ls, v.DN) 456 } 457 if isAttributeSSHPublicKeySet { 458 result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey) 459 } 460 if isAtributeAvatarSet { 461 result[i].Avatar = v.GetRawAttributeValue(ls.AttributeAvatar) 462 } 463 result[i].LowerName = strings.ToLower(result[i].Username) 464 } 465 466 return result, nil 467} 468