ldap.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. package service
  2. import (
  3. "crypto/tls"
  4. "crypto/x509"
  5. "errors"
  6. "fmt"
  7. "net/url"
  8. "os"
  9. "strconv"
  10. "strings"
  11. "github.com/go-ldap/ldap/v3"
  12. "github.com/lejianwen/rustdesk-api/v2/config"
  13. "github.com/lejianwen/rustdesk-api/v2/model"
  14. )
  15. var (
  16. ErrUrlParseFailed = errors.New("UrlParseFailed")
  17. ErrFileReadFailed = errors.New("FileReadFailed")
  18. ErrLdapNotEnabled = errors.New("LdapNotEnabled")
  19. ErrLdapUserDisabled = errors.New("UserDisabledAtLdap")
  20. ErrLdapUserNotFound = errors.New("UserNotFound")
  21. ErrLdapMailNotMatch = errors.New("MailNotMatch")
  22. ErrLdapConnectFailed = errors.New("LdapConnectFailed")
  23. ErrLdapSearchFailed = errors.New("LdapSearchRequestFailed")
  24. ErrLdapTlsFailed = errors.New("LdapStartTLSFailed")
  25. ErrLdapBindService = errors.New("LdapBindServiceFailed")
  26. ErrLdapBindFailed = errors.New("LdapBindFailed")
  27. ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
  28. ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
  29. ErrLdapPasswordNotMatch = errors.New("PasswordNotMatch")
  30. )
  31. // LdapService is responsible for LDAP authentication and user synchronization.
  32. type LdapService struct {
  33. }
  34. // LdapUser represents the user attributes retrieved from LDAP.
  35. type LdapUser struct {
  36. Dn string
  37. Username string
  38. Email string
  39. FirstName string
  40. LastName string
  41. MemberOf []string
  42. EnableAttrValue string
  43. Enabled bool
  44. }
  45. // Name returns the full name of an LDAP user.
  46. func (lu *LdapUser) Name() string {
  47. return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
  48. }
  49. // ToUser merges the LdapUser data into a provided *model.User.
  50. // If 'u' is nil, it creates and returns a new *model.User.
  51. func (lu *LdapUser) ToUser(u *model.User) *model.User {
  52. if u == nil {
  53. u = &model.User{}
  54. }
  55. u.Username = lu.Username
  56. u.Email = lu.Email
  57. u.Nickname = lu.Name()
  58. if lu.Enabled {
  59. u.Status = model.COMMON_STATUS_ENABLE
  60. } else {
  61. u.Status = model.COMMON_STATUS_DISABLED
  62. }
  63. return u
  64. }
  65. // connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
  66. func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
  67. u, err := url.Parse(cfg.Url)
  68. if err != nil {
  69. return nil, errors.Join(ErrUrlParseFailed, err)
  70. }
  71. var conn *ldap.Conn
  72. if u.Scheme == "ldaps" {
  73. // WARNING: InsecureSkipVerify: true is not recommended for production
  74. tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.TlsVerify}
  75. if cfg.TlsCaFile != "" {
  76. caCert, err := os.ReadFile(cfg.TlsCaFile)
  77. if err != nil {
  78. return nil, errors.Join(ErrFileReadFailed, err)
  79. }
  80. caCertPool := x509.NewCertPool()
  81. if !caCertPool.AppendCertsFromPEM(caCert) {
  82. return nil, errors.Join(ErrLdapTlsFailed, errors.New("failed to append CA certificate"))
  83. }
  84. tlsConfig.RootCAs = caCertPool
  85. }
  86. conn, err = ldap.DialURL(cfg.Url, ldap.DialWithTLSConfig(tlsConfig))
  87. } else {
  88. conn, err = ldap.DialURL(cfg.Url)
  89. }
  90. if err != nil {
  91. return nil, errors.Join(ErrLdapConnectFailed, err)
  92. }
  93. // Bind as the "service" user
  94. if err = conn.Bind(username, password); err != nil {
  95. fmt.Println("Bind failed")
  96. conn.Close()
  97. return nil, errors.Join(ErrLdapBindService, err)
  98. }
  99. return conn, nil
  100. }
  101. // connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
  102. func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
  103. return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
  104. }
  105. // verifyCredentials checks the provided username and password against LDAP.
  106. func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
  107. ldapConn, err := ls.connectAndBind(cfg, username, password)
  108. if err != nil {
  109. return ErrLdapPasswordNotMatch
  110. }
  111. defer ldapConn.Close()
  112. return nil
  113. }
  114. // Authenticate checks the provided username and password against LDAP.
  115. // Returns the corresponding *model.User if successful, or an error if not.
  116. func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
  117. ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
  118. if err != nil {
  119. return nil, err
  120. }
  121. if !ldapUser.Enabled {
  122. return nil, ErrLdapUserDisabled
  123. }
  124. cfg := &Config.Ldap
  125. // Skip allow-group check for admins
  126. isAdmin := ls.isUserAdmin(cfg, ldapUser)
  127. // non-admins only check if allow-group is configured
  128. if !isAdmin && cfg.User.AllowGroup != "" {
  129. if !ls.isUserInGroup(cfg, ldapUser, cfg.User.AllowGroup) {
  130. return nil, errors.New("user not in allowed group")
  131. }
  132. }
  133. err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
  134. if err != nil {
  135. return nil, err
  136. }
  137. user, err := ls.mapToLocalUser(cfg, ldapUser)
  138. if err != nil {
  139. return nil, errors.Join(ErrLdapToLocalUserFailed, err)
  140. }
  141. return user, nil
  142. }
  143. // isUserInGroup checks if the user is a member of the specified group. by_sw
  144. func (ls *LdapService) isUserInGroup(cfg *config.Ldap, ldapUser *LdapUser, groupDN string) bool {
  145. // Check "memberOf" directly
  146. if len(ldapUser.MemberOf) > 0 {
  147. for _, group := range ldapUser.MemberOf {
  148. if strings.EqualFold(group, groupDN) {
  149. return true
  150. }
  151. }
  152. }
  153. // For "member" attribute, perform a reverse search on the group
  154. member := "member"
  155. userDN := ldap.EscapeFilter(ldapUser.Dn)
  156. groupDN = ldap.EscapeFilter(groupDN)
  157. groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
  158. // Create the LDAP search request
  159. groupSearchRequest := ldap.NewSearchRequest(
  160. groupDN,
  161. ldap.ScopeWholeSubtree,
  162. ldap.NeverDerefAliases,
  163. 0, // Unlimited search results
  164. 0, // No time limit
  165. false, // Return both attributes and DN
  166. groupFilter,
  167. []string{"dn"},
  168. nil,
  169. )
  170. // Perform the group search
  171. groupResult, err := ls.searchResult(cfg, groupSearchRequest)
  172. if err != nil {
  173. return false
  174. }
  175. // If any results are returned, the user is part of the group
  176. return len(groupResult.Entries) > 0
  177. }
  178. // mapToLocalUser checks whether the user exists locally; if not, creates one.
  179. // If the user exists and Ldap.Sync is enabled, it updates local info.
  180. func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
  181. userService := &UserService{}
  182. localUser := userService.InfoByUsername(lu.Username)
  183. isAdmin := ls.isUserAdmin(cfg, lu)
  184. // If the user doesn't exist in local DB, create a new one
  185. if localUser.Id == 0 {
  186. newUser := lu.ToUser(nil)
  187. // Typically, you don’t store LDAP user passwords locally.
  188. // If needed, you can set a random password here.
  189. newUser.IsAdmin = &isAdmin
  190. newUser.GroupId = 1
  191. if err := DB.Create(newUser).Error; err != nil {
  192. return nil, errors.Join(ErrLdapCreateUserFailed, err)
  193. }
  194. return userService.InfoByUsername(lu.Username), nil
  195. }
  196. // If the user already exists and sync is enabled, update local info
  197. if cfg.User.Sync {
  198. originalEmail := localUser.Email
  199. originalNickname := localUser.Nickname
  200. originalIsAdmin := localUser.IsAdmin
  201. originalStatus := localUser.Status
  202. lu.ToUser(localUser) // merges LDAP data into the existing user
  203. localUser.IsAdmin = &isAdmin
  204. if err := userService.Update(localUser); err != nil {
  205. // If the update fails, revert to original data
  206. localUser.Email = originalEmail
  207. localUser.Nickname = originalNickname
  208. localUser.IsAdmin = originalIsAdmin
  209. localUser.Status = originalStatus
  210. }
  211. }
  212. return localUser, nil
  213. }
  214. // IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
  215. func (ls *LdapService) IsUsernameExists(username string) bool {
  216. cfg := &Config.Ldap
  217. if !cfg.Enable {
  218. return false
  219. }
  220. sr, err := ls.usernameSearchResult(cfg, username)
  221. if err != nil {
  222. return false
  223. }
  224. return len(sr.Entries) > 0
  225. }
  226. // IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
  227. func (ls *LdapService) IsEmailExists(email string) bool {
  228. cfg := &Config.Ldap
  229. if !cfg.Enable {
  230. return false
  231. }
  232. sr, err := ls.emailSearchResult(cfg, email)
  233. if err != nil {
  234. return false
  235. }
  236. return len(sr.Entries) > 0
  237. }
  238. // GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
  239. func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
  240. cfg := &Config.Ldap
  241. if !cfg.Enable {
  242. return nil, ErrLdapNotEnabled
  243. }
  244. sr, err := ls.usernameSearchResult(cfg, username)
  245. if err != nil {
  246. return nil, errors.Join(ErrLdapSearchFailed, err)
  247. }
  248. if len(sr.Entries) != 1 {
  249. return nil, ErrLdapUserNotFound
  250. }
  251. return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
  252. }
  253. // GetUserInfoByUsernameLocal returns the user info from LDAP for the given username. If the user exists, it will sync the user info to the local database.
  254. func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User, error) {
  255. ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
  256. if err != nil {
  257. return &model.User{}, err
  258. }
  259. return ls.mapToLocalUser(&Config.Ldap, ldapUser)
  260. }
  261. // GetUserInfoByEmailLdap returns the user info from LDAP for the given email.
  262. func (ls *LdapService) GetUserInfoByEmailLdap(email string) (*LdapUser, error) {
  263. cfg := &Config.Ldap
  264. if !cfg.Enable {
  265. return nil, ErrLdapNotEnabled
  266. }
  267. sr, err := ls.emailSearchResult(cfg, email)
  268. if err != nil {
  269. return nil, errors.Join(ErrLdapSearchFailed, err)
  270. }
  271. if len(sr.Entries) != 1 {
  272. return nil, ErrLdapUserNotFound
  273. }
  274. return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
  275. }
  276. // GetUserInfoByEmailLocal returns the user info from LDAP for the given email. if the user exists, it will synchronize the user information to local database.
  277. func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error) {
  278. ldapUser, err := ls.GetUserInfoByEmailLdap(email)
  279. if err != nil {
  280. return &model.User{}, err
  281. }
  282. return ls.mapToLocalUser(&Config.Ldap, ldapUser)
  283. }
  284. // usernameSearchResult returns the search result for the given username.
  285. func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
  286. // Build the combined filter for the username
  287. filter := ls.filterField(ls.fieldUsername(cfg), username)
  288. // Create the *ldap.SearchRequest
  289. searchRequest := ls.buildUserSearchRequest(cfg, filter)
  290. return ls.searchResult(cfg, searchRequest)
  291. }
  292. // emailSearchResult returns the search result for the given email.
  293. func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
  294. filter := ls.filterField(ls.fieldEmail(cfg), email)
  295. searchRequest := ls.buildUserSearchRequest(cfg, filter)
  296. return ls.searchResult(cfg, searchRequest)
  297. }
  298. func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
  299. ldapConn, err := ls.connectAndBindAdmin(cfg)
  300. if err != nil {
  301. return nil, err
  302. }
  303. defer ldapConn.Close()
  304. return ldapConn.Search(searchRequest)
  305. }
  306. // buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
  307. func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
  308. baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
  309. filterConfig := cfg.User.Filter
  310. if filterConfig == "" {
  311. filterConfig = "(cn=*)"
  312. }
  313. // Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
  314. combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
  315. attributes := ls.buildUserAttributes(cfg)
  316. return ldap.NewSearchRequest(
  317. baseDn,
  318. ldap.ScopeWholeSubtree,
  319. ldap.NeverDerefAliases,
  320. 0, // unlimited search results
  321. 0, // no server-side time limit
  322. false, // typesOnly
  323. combinedFilter,
  324. attributes,
  325. nil,
  326. )
  327. }
  328. // buildUserAttributes returns the list of attributes we want from LDAP user searches.
  329. func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
  330. return []string{
  331. "dn",
  332. ls.fieldUsername(cfg),
  333. ls.fieldEmail(cfg),
  334. ls.fieldFirstName(cfg),
  335. ls.fieldLastName(cfg),
  336. ls.fieldMemberOf(),
  337. ls.fieldUserEnableAttr(cfg),
  338. }
  339. }
  340. // userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
  341. func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
  342. lu := &LdapUser{
  343. Dn: entry.DN,
  344. Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
  345. Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
  346. FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
  347. LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
  348. MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
  349. EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
  350. }
  351. // Check if the user is enabled based on the LDAP configuration
  352. ls.isUserEnabled(cfg, lu)
  353. return lu
  354. }
  355. // filterField helps build simple attribute filters, e.g. (uid=username).
  356. func (ls *LdapService) filterField(field, value string) string {
  357. return fmt.Sprintf("(%s=%s)", field, value)
  358. }
  359. // fieldUsername returns the configured username attribute or "uid" if not set.
  360. func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
  361. if cfg.User.Username == "" {
  362. return "uid"
  363. }
  364. return cfg.User.Username
  365. }
  366. // fieldEmail returns the configured email attribute or "mail" if not set.
  367. func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
  368. if cfg.User.Email == "" {
  369. return "mail"
  370. }
  371. return cfg.User.Email
  372. }
  373. // fieldFirstName returns the configured first name attribute or "givenName" if not set.
  374. func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
  375. if cfg.User.FirstName == "" {
  376. return "givenName"
  377. }
  378. return cfg.User.FirstName
  379. }
  380. // fieldLastName returns the configured last name attribute or "sn" if not set.
  381. func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
  382. if cfg.User.LastName == "" {
  383. return "sn"
  384. }
  385. return cfg.User.LastName
  386. }
  387. func (ls *LdapService) fieldMemberOf() string {
  388. return "memberOf"
  389. }
  390. func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
  391. if cfg.User.EnableAttr == "" {
  392. return "userAccountControl"
  393. }
  394. return cfg.User.EnableAttr
  395. }
  396. // baseDnUser returns the user-specific base DN or the global base DN if none is set.
  397. func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
  398. if cfg.User.BaseDn == "" {
  399. return cfg.BaseDn
  400. }
  401. return cfg.User.BaseDn
  402. }
  403. // isUserAdmin checks if the user is a member of the admin group.
  404. func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
  405. // Check if the admin group is configured
  406. adminGroup := cfg.User.AdminGroup
  407. if adminGroup == "" {
  408. return false
  409. }
  410. // Check "memberOf" directly
  411. if len(ldapUser.MemberOf) > 0 {
  412. for _, group := range ldapUser.MemberOf {
  413. if strings.EqualFold(group, adminGroup) {
  414. return true
  415. }
  416. }
  417. return false
  418. }
  419. // For "member" attribute, perform a reverse search on the group
  420. member := "member"
  421. userDN := ldap.EscapeFilter(ldapUser.Dn)
  422. adminGroupDn := ldap.EscapeFilter(adminGroup)
  423. groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
  424. // Create the LDAP search request
  425. groupSearchRequest := ldap.NewSearchRequest(
  426. adminGroupDn,
  427. ldap.ScopeWholeSubtree,
  428. ldap.NeverDerefAliases,
  429. 0, // Unlimited search results
  430. 0, // No time limit
  431. false, // Return both attributes and DN
  432. groupFilter,
  433. []string{"dn"},
  434. nil,
  435. )
  436. // Perform the group search
  437. groupResult, err := ls.searchResult(cfg, groupSearchRequest)
  438. if err != nil {
  439. return false
  440. }
  441. // If any results are returned, the user is part of the admin group
  442. if len(groupResult.Entries) > 0 {
  443. return true
  444. }
  445. return false
  446. }
  447. // isUserEnabled checks if the user is enabled based on the LDAP configuration.
  448. // If no enable attribute or value is set, all users are considered enabled by default.
  449. func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
  450. // Retrieve the enable attribute and expected value from the configuration
  451. enableAttr := cfg.User.EnableAttr
  452. enableAttrValue := cfg.User.EnableAttrValue
  453. // If no enable attribute or value is configured, consider all users as enabled
  454. if enableAttr == "" || enableAttrValue == "" {
  455. ldapUser.Enabled = true
  456. return true
  457. }
  458. // Normalize the enable attribute for comparison
  459. enableAttr = strings.ToLower(enableAttr)
  460. // Handle Active Directory's userAccountControl attribute
  461. if enableAttr == "useraccountcontrol" {
  462. // Parse the userAccountControl value
  463. userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
  464. if err != nil {
  465. fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
  466. ldapUser.Enabled = false
  467. return false
  468. }
  469. // Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
  470. const ACCOUNTDISABLE = 0x2
  471. ldapUser.Enabled = userAccountControl&ACCOUNTDISABLE == 0
  472. return ldapUser.Enabled
  473. }
  474. // For other attributes, perform a direct comparison with the expected value
  475. ldapUser.Enabled = ldapUser.EnableAttrValue == enableAttrValue
  476. return ldapUser.Enabled
  477. }
  478. // getAttrOfDn retrieves the value of an attribute for a given DN.
  479. func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
  480. searchRequest := ldap.NewSearchRequest(
  481. ldap.EscapeFilter(dn),
  482. ldap.ScopeBaseObject,
  483. ldap.NeverDerefAliases,
  484. 0, // unlimited search results
  485. 0, // no server-side time limit
  486. false, // typesOnly
  487. "(objectClass=*)",
  488. []string{attr},
  489. nil,
  490. )
  491. sr, err := ls.searchResult(cfg, searchRequest)
  492. if err != nil {
  493. return ""
  494. }
  495. if len(sr.Entries) == 0 {
  496. return ""
  497. }
  498. return sr.Entries[0].GetAttributeValue(attr)
  499. }