You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

source_search.go 17KB

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