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

ldap.go 9.7 kB

12 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. // Package ldap provide functions & structure to query a LDAP ldap directory
  5. // For now, it's mainly tested again an MS Active Directory service, see README.md for more information
  6. package ldap
  7. import (
  8. "crypto/tls"
  9. "fmt"
  10. "strings"
  11. "gopkg.in/ldap.v2"
  12. "code.gitea.io/gitea/modules/log"
  13. )
  14. // SecurityProtocol protocol type
  15. type SecurityProtocol int
  16. // Note: new type must be added at the end of list to maintain compatibility.
  17. const (
  18. SecurityProtocolUnencrypted SecurityProtocol = iota
  19. SecurityProtocolLDAPS
  20. SecurityProtocolStartTLS
  21. )
  22. // Source Basic LDAP authentication service
  23. type Source struct {
  24. Name string // canonical name (ie. corporate.ad)
  25. Host string // LDAP host
  26. Port int // port number
  27. SecurityProtocol SecurityProtocol
  28. SkipVerify bool
  29. BindDN string // DN to bind with
  30. BindPassword string // Bind DN password
  31. UserBase string // Base search path for users
  32. UserDN string // Template for the DN of the user for simple auth
  33. AttributeUsername string // Username attribute
  34. AttributeName string // First name attribute
  35. AttributeSurname string // Surname attribute
  36. AttributeMail string // E-mail attribute
  37. AttributesInBind bool // fetch attributes in bind context (not user)
  38. SearchPageSize uint32 // Search with paging page size
  39. Filter string // Query filter to validate entry
  40. AdminFilter string // Query filter to check if user is admin
  41. Enabled bool // if this source is disabled
  42. }
  43. // SearchResult : user data
  44. type SearchResult struct {
  45. Username string // Username
  46. Name string // Name
  47. Surname string // Surname
  48. Mail string // E-mail address
  49. IsAdmin bool // if user is administrator
  50. }
  51. func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
  52. // See http://tools.ietf.org/search/rfc4515
  53. badCharacters := "\x00()*\\"
  54. if strings.ContainsAny(username, badCharacters) {
  55. log.Debug("'%s' contains invalid query characters. Aborting.", username)
  56. return "", false
  57. }
  58. return fmt.Sprintf(ls.Filter, username), true
  59. }
  60. func (ls *Source) sanitizedUserDN(username string) (string, bool) {
  61. // See http://tools.ietf.org/search/rfc4514: "special characters"
  62. badCharacters := "\x00()*\\,='\"#+;<>"
  63. if strings.ContainsAny(username, badCharacters) {
  64. log.Debug("'%s' contains invalid DN characters. Aborting.", username)
  65. return "", false
  66. }
  67. return fmt.Sprintf(ls.UserDN, username), true
  68. }
  69. func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
  70. log.Trace("Search for LDAP user: %s", name)
  71. if ls.BindDN != "" && ls.BindPassword != "" {
  72. err := l.Bind(ls.BindDN, ls.BindPassword)
  73. if err != nil {
  74. log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
  75. return "", false
  76. }
  77. log.Trace("Bound as BindDN %s", ls.BindDN)
  78. } else {
  79. log.Trace("Proceeding with anonymous LDAP search.")
  80. }
  81. // A search for the user.
  82. userFilter, ok := ls.sanitizedUserQuery(name)
  83. if !ok {
  84. return "", false
  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. // Ensure we found a user
  91. sr, err := l.Search(search)
  92. if err != nil || len(sr.Entries) < 1 {
  93. log.Debug("Failed search using filter[%s]: %v", userFilter, err)
  94. return "", false
  95. } else if len(sr.Entries) > 1 {
  96. log.Debug("Filter '%s' returned more than one user.", userFilter)
  97. return "", false
  98. }
  99. userDN := sr.Entries[0].DN
  100. if userDN == "" {
  101. log.Error(4, "LDAP search was successful, but found no DN!")
  102. return "", false
  103. }
  104. return userDN, true
  105. }
  106. func dial(ls *Source) (*ldap.Conn, error) {
  107. log.Trace("Dialing LDAP with security protocol (%v) without verifying: %v", ls.SecurityProtocol, ls.SkipVerify)
  108. tlsCfg := &tls.Config{
  109. ServerName: ls.Host,
  110. InsecureSkipVerify: ls.SkipVerify,
  111. }
  112. if ls.SecurityProtocol == SecurityProtocolLDAPS {
  113. return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), tlsCfg)
  114. }
  115. conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port))
  116. if err != nil {
  117. return nil, fmt.Errorf("Dial: %v", err)
  118. }
  119. if ls.SecurityProtocol == SecurityProtocolStartTLS {
  120. if err = conn.StartTLS(tlsCfg); err != nil {
  121. conn.Close()
  122. return nil, fmt.Errorf("StartTLS: %v", err)
  123. }
  124. }
  125. return conn, nil
  126. }
  127. func bindUser(l *ldap.Conn, userDN, passwd string) error {
  128. log.Trace("Binding with userDN: %s", userDN)
  129. err := l.Bind(userDN, passwd)
  130. if err != nil {
  131. log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
  132. return err
  133. }
  134. log.Trace("Bound successfully with userDN: %s", userDN)
  135. return err
  136. }
  137. func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
  138. if len(ls.AdminFilter) > 0 {
  139. log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
  140. search := ldap.NewSearchRequest(
  141. userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
  142. []string{ls.AttributeName},
  143. nil)
  144. sr, err := l.Search(search)
  145. if err != nil {
  146. log.Error(4, "LDAP Admin Search failed unexpectedly! (%v)", err)
  147. } else if len(sr.Entries) < 1 {
  148. log.Error(4, "LDAP Admin Search failed")
  149. } else {
  150. return true
  151. }
  152. }
  153. return false
  154. }
  155. // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
  156. func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
  157. // See https://tools.ietf.org/search/rfc4513#section-5.1.2
  158. if len(passwd) == 0 {
  159. log.Debug("Auth. failed for %s, password cannot be empty")
  160. return nil
  161. }
  162. l, err := dial(ls)
  163. if err != nil {
  164. log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
  165. ls.Enabled = false
  166. return nil
  167. }
  168. defer l.Close()
  169. var userDN string
  170. if directBind {
  171. log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
  172. var ok bool
  173. userDN, ok = ls.sanitizedUserDN(name)
  174. if !ok {
  175. return nil
  176. }
  177. } else {
  178. log.Trace("LDAP will use BindDN.")
  179. var found bool
  180. userDN, found = ls.findUserDN(l, name)
  181. if !found {
  182. return nil
  183. }
  184. }
  185. if directBind || !ls.AttributesInBind {
  186. // binds user (checking password) before looking-up attributes in user context
  187. err = bindUser(l, userDN, passwd)
  188. if err != nil {
  189. return nil
  190. }
  191. }
  192. userFilter, ok := ls.sanitizedUserQuery(name)
  193. if !ok {
  194. return nil
  195. }
  196. log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, userDN)
  197. search := ldap.NewSearchRequest(
  198. userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
  199. []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},
  200. nil)
  201. sr, err := l.Search(search)
  202. if err != nil {
  203. log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
  204. return nil
  205. } else if len(sr.Entries) < 1 {
  206. if directBind {
  207. log.Error(4, "User filter inhibited user login.")
  208. } else {
  209. log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
  210. }
  211. return nil
  212. }
  213. username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
  214. firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
  215. surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
  216. mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
  217. isAdmin := checkAdmin(l, ls, userDN)
  218. if !directBind && ls.AttributesInBind {
  219. // binds user (checking password) after looking-up attributes in BindDN context
  220. err = bindUser(l, userDN, passwd)
  221. if err != nil {
  222. return nil
  223. }
  224. }
  225. return &SearchResult{
  226. Username: username,
  227. Name: firstname,
  228. Surname: surname,
  229. Mail: mail,
  230. IsAdmin: isAdmin,
  231. }
  232. }
  233. // UsePagedSearch returns if need to use paged search
  234. func (ls *Source) UsePagedSearch() bool {
  235. return ls.SearchPageSize > 0
  236. }
  237. // SearchEntries : search an LDAP source for all users matching userFilter
  238. func (ls *Source) SearchEntries() []*SearchResult {
  239. l, err := dial(ls)
  240. if err != nil {
  241. log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
  242. ls.Enabled = false
  243. return nil
  244. }
  245. defer l.Close()
  246. if ls.BindDN != "" && ls.BindPassword != "" {
  247. err := l.Bind(ls.BindDN, ls.BindPassword)
  248. if err != nil {
  249. log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
  250. return nil
  251. }
  252. log.Trace("Bound as BindDN %s", ls.BindDN)
  253. } else {
  254. log.Trace("Proceeding with anonymous LDAP search.")
  255. }
  256. userFilter := fmt.Sprintf(ls.Filter, "*")
  257. log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, ls.UserBase)
  258. search := ldap.NewSearchRequest(
  259. ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
  260. []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},
  261. nil)
  262. var sr *ldap.SearchResult
  263. if ls.UsePagedSearch() {
  264. sr, err = l.SearchWithPaging(search, ls.SearchPageSize)
  265. } else {
  266. sr, err = l.Search(search)
  267. }
  268. if err != nil {
  269. log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
  270. return nil
  271. }
  272. result := make([]*SearchResult, len(sr.Entries))
  273. for i, v := range sr.Entries {
  274. result[i] = &SearchResult{
  275. Username: v.GetAttributeValue(ls.AttributeUsername),
  276. Name: v.GetAttributeValue(ls.AttributeName),
  277. Surname: v.GetAttributeValue(ls.AttributeSurname),
  278. Mail: v.GetAttributeValue(ls.AttributeMail),
  279. IsAdmin: checkAdmin(l, ls, v.DN),
  280. }
  281. }
  282. return result
  283. }