ldaputils.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package ldaputils
  15. import (
  16. "crypto/tls"
  17. "encoding/hex"
  18. "fmt"
  19. "strings"
  20. "github.com/go-ldap/ldap/v3"
  21. "yunion.io/x/log"
  22. "yunion.io/x/pkg/errors"
  23. )
  24. var (
  25. ErrUserNotFound = errors.Error("not found")
  26. ErrUserDuplicate = errors.Error("user id duplicate")
  27. ErrUserBadCredential = errors.Error("bad credential")
  28. binaryAttributes = []string{
  29. "objectGUID",
  30. "objectSid",
  31. }
  32. )
  33. type SLDAPClient struct {
  34. url string
  35. account string
  36. password string
  37. baseDN string
  38. isDebug bool
  39. conn *ldap.Conn
  40. }
  41. func NewLDAPClient(url, account, password string, baseDN string, isDebug bool) *SLDAPClient {
  42. return &SLDAPClient{
  43. url: url,
  44. account: account,
  45. password: password,
  46. baseDN: baseDN,
  47. isDebug: isDebug,
  48. }
  49. }
  50. func (cli *SLDAPClient) Connect() error {
  51. var err error
  52. cli.conn, err = ldap.DialURL(cli.url, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
  53. if err != nil {
  54. return errors.Wrap(err, "DiaURL")
  55. }
  56. return cli.bind()
  57. }
  58. func (cli *SLDAPClient) bind() error {
  59. if len(cli.account) > 0 {
  60. err := cli.conn.Bind(cli.account, cli.password)
  61. if err != nil {
  62. return errors.Wrap(err, "Bind")
  63. }
  64. }
  65. return nil
  66. }
  67. func (cli *SLDAPClient) Close() {
  68. if cli.conn != nil {
  69. cli.conn.Close()
  70. cli.conn = nil
  71. }
  72. }
  73. func (cli *SLDAPClient) Authenticate(baseDN string, objClass string, uidAttr string, uname string, passwd string, filter string, fields []string, queryScope int) (*ldap.Entry, error) {
  74. attrMap := make(map[string]string)
  75. attrMap[uidAttr] = uname
  76. var retEntry *ldap.Entry
  77. total, err := cli.Search(
  78. baseDN, objClass, attrMap, filter, fields, queryScope,
  79. 2, 2,
  80. func(offset uint32, entry *ldap.Entry) error {
  81. retEntry = entry
  82. return nil
  83. },
  84. )
  85. if err != nil {
  86. return nil, errors.Wrap(err, "Search")
  87. }
  88. if total == 0 {
  89. return nil, ErrUserNotFound
  90. }
  91. if total > 1 {
  92. return nil, ErrUserDuplicate
  93. }
  94. defer cli.bind()
  95. err = cli.conn.Bind(retEntry.DN, passwd)
  96. if err != nil {
  97. return nil, ErrUserBadCredential
  98. }
  99. return retEntry, nil
  100. }
  101. const (
  102. StopSearch = errors.Error("stop dap search")
  103. )
  104. // support pagination
  105. // reference: https://zerokspot.com/weblog/2018/03/07/paging-in-gopkg-ldap/
  106. func (cli *SLDAPClient) Search(
  107. base string, objClass string,
  108. condition map[string]string,
  109. filter string,
  110. fields []string,
  111. queryScope int,
  112. pageSize uint32, limit uint32,
  113. entryFunc func(offset uint32, entry *ldap.Entry) error,
  114. ) (uint32, error) {
  115. searches := strings.Builder{}
  116. if len(condition) == 0 && len(objClass) == 0 {
  117. searches.WriteString("(objectClass=*)")
  118. }
  119. if len(objClass) > 0 {
  120. searches.WriteString("(objectClass=")
  121. searches.WriteString(objClass)
  122. searches.WriteString(")")
  123. }
  124. for k, v := range condition {
  125. searches.WriteString("(")
  126. searches.WriteString(k)
  127. searches.WriteString("=")
  128. if isBinaryAttr(k) {
  129. v = toBinarySearchString(v)
  130. }
  131. searches.WriteString(v)
  132. searches.WriteString(")")
  133. }
  134. if len(filter) > 0 && strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") {
  135. searches.WriteString(filter)
  136. }
  137. searchStr := fmt.Sprintf("(&%s)", searches.String())
  138. if len(base) == 0 {
  139. base = cli.baseDN
  140. }
  141. if queryScope != ldap.ScopeWholeSubtree && queryScope != ldap.ScopeSingleLevel && queryScope != ldap.ScopeBaseObject {
  142. queryScope = ldap.ScopeWholeSubtree
  143. }
  144. log.Debugf("ldapSearch: %s", searchStr)
  145. offset := uint32(0)
  146. paging := ldap.NewControlPaging(pageSize)
  147. for {
  148. searchRequest := ldap.NewSearchRequest(
  149. base, // The base dn to search
  150. queryScope, ldap.NeverDerefAliases, 0, 0, false,
  151. searchStr,
  152. fields, // A list attributes to retrieve
  153. []ldap.Control{paging},
  154. )
  155. searchResult, err := cli.conn.Search(searchRequest)
  156. if err != nil {
  157. return offset, errors.Wrap(err, "Seearch")
  158. }
  159. pageTotal := len(searchResult.Entries)
  160. for i := 0; i < pageTotal; i++ {
  161. entry := searchResult.Entries[i]
  162. err := entryFunc(offset, entry)
  163. if err != nil && errors.Cause(err) != StopSearch {
  164. return offset, errors.Wrapf(err, "process entry fail at %d-%d-%d", pageTotal, i, offset)
  165. }
  166. offset += 1
  167. if (err != nil && errors.Cause(err) == StopSearch) || (limit > 0 && offset >= limit) {
  168. // stop, offset exceeds limit or receive StopSearch error
  169. return offset, nil
  170. }
  171. }
  172. resultCtrl := ldap.FindControl(searchResult.Controls, paging.GetControlType())
  173. if resultCtrl == nil {
  174. break
  175. }
  176. if pagingCtrl, ok := resultCtrl.(*ldap.ControlPaging); ok {
  177. if len(pagingCtrl.Cookie) == 0 {
  178. break
  179. }
  180. paging.SetCookie(pagingCtrl.Cookie)
  181. }
  182. }
  183. return offset, nil
  184. }
  185. func isBinaryAttr(attrName string) bool {
  186. for _, attr := range binaryAttributes {
  187. if strings.EqualFold(attr, attrName) {
  188. return true
  189. }
  190. }
  191. return false
  192. }
  193. func toBinary(val string) string {
  194. ret, err := hex.DecodeString(val)
  195. if err != nil {
  196. return val
  197. } else {
  198. return string(ret)
  199. }
  200. }
  201. func toBinarySearchString(val string) string {
  202. ret := strings.Builder{}
  203. for i := 0; i < len(val); i += 2 {
  204. ret.WriteString(`\`)
  205. ret.WriteString(val[i : i+2])
  206. }
  207. return ret.String()
  208. }
  209. func toHex(val string) string {
  210. return hex.EncodeToString([]byte(val))
  211. }
  212. func GetAttributeValue(e *ldap.Entry, key string) string {
  213. val := e.GetAttributeValue(key)
  214. if isBinaryAttr(key) {
  215. val = toHex(val)
  216. }
  217. return val
  218. }
  219. func GetAttributeValues(e *ldap.Entry, key string) []string {
  220. vals := e.GetAttributeValues(key)
  221. if isBinaryAttr(key) {
  222. for i := range vals {
  223. vals[i] = toHex(vals[i])
  224. }
  225. }
  226. return vals
  227. }