| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- package service
- import (
- "crypto/tls"
- "crypto/x509"
- "errors"
- "fmt"
- "net/url"
- "os"
- "strconv"
- "strings"
- "github.com/go-ldap/ldap/v3"
- "github.com/lejianwen/rustdesk-api/v2/config"
- "github.com/lejianwen/rustdesk-api/v2/model"
- )
- var (
- ErrUrlParseFailed = errors.New("UrlParseFailed")
- ErrFileReadFailed = errors.New("FileReadFailed")
- ErrLdapNotEnabled = errors.New("LdapNotEnabled")
- ErrLdapUserDisabled = errors.New("UserDisabledAtLdap")
- ErrLdapUserNotFound = errors.New("UserNotFound")
- ErrLdapMailNotMatch = errors.New("MailNotMatch")
- ErrLdapConnectFailed = errors.New("LdapConnectFailed")
- ErrLdapSearchFailed = errors.New("LdapSearchRequestFailed")
- ErrLdapTlsFailed = errors.New("LdapStartTLSFailed")
- ErrLdapBindService = errors.New("LdapBindServiceFailed")
- ErrLdapBindFailed = errors.New("LdapBindFailed")
- ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
- ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
- ErrLdapPasswordNotMatch = errors.New("PasswordNotMatch")
- )
- // LdapService is responsible for LDAP authentication and user synchronization.
- type LdapService struct {
- }
- // LdapUser represents the user attributes retrieved from LDAP.
- type LdapUser struct {
- Dn string
- Username string
- Email string
- FirstName string
- LastName string
- MemberOf []string
- EnableAttrValue string
- Enabled bool
- }
- // Name returns the full name of an LDAP user.
- func (lu *LdapUser) Name() string {
- return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
- }
- // ToUser merges the LdapUser data into a provided *model.User.
- // If 'u' is nil, it creates and returns a new *model.User.
- func (lu *LdapUser) ToUser(u *model.User) *model.User {
- if u == nil {
- u = &model.User{}
- }
- u.Username = lu.Username
- u.Email = lu.Email
- u.Nickname = lu.Name()
- if lu.Enabled {
- u.Status = model.COMMON_STATUS_ENABLE
- } else {
- u.Status = model.COMMON_STATUS_DISABLED
- }
- return u
- }
- // connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
- func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
- u, err := url.Parse(cfg.Url)
- if err != nil {
- return nil, errors.Join(ErrUrlParseFailed, err)
- }
- var conn *ldap.Conn
- if u.Scheme == "ldaps" {
- // WARNING: InsecureSkipVerify: true is not recommended for production
- tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.TlsVerify}
- if cfg.TlsCaFile != "" {
- caCert, err := os.ReadFile(cfg.TlsCaFile)
- if err != nil {
- return nil, errors.Join(ErrFileReadFailed, err)
- }
- caCertPool := x509.NewCertPool()
- if !caCertPool.AppendCertsFromPEM(caCert) {
- return nil, errors.Join(ErrLdapTlsFailed, errors.New("failed to append CA certificate"))
- }
- tlsConfig.RootCAs = caCertPool
- }
- conn, err = ldap.DialURL(cfg.Url, ldap.DialWithTLSConfig(tlsConfig))
- } else {
- conn, err = ldap.DialURL(cfg.Url)
- }
- if err != nil {
- return nil, errors.Join(ErrLdapConnectFailed, err)
- }
- // Bind as the "service" user
- if err = conn.Bind(username, password); err != nil {
- fmt.Println("Bind failed")
- conn.Close()
- return nil, errors.Join(ErrLdapBindService, err)
- }
- return conn, nil
- }
- // connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
- func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
- return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
- }
- // verifyCredentials checks the provided username and password against LDAP.
- func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
- ldapConn, err := ls.connectAndBind(cfg, username, password)
- if err != nil {
- return ErrLdapPasswordNotMatch
- }
- defer ldapConn.Close()
- return nil
- }
- // Authenticate checks the provided username and password against LDAP.
- // Returns the corresponding *model.User if successful, or an error if not.
- func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
- ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
- if err != nil {
- return nil, err
- }
- if !ldapUser.Enabled {
- return nil, ErrLdapUserDisabled
- }
- cfg := &Config.Ldap
- // Skip allow-group check for admins
- isAdmin := ls.isUserAdmin(cfg, ldapUser)
-
- // non-admins only check if allow-group is configured
- if !isAdmin && cfg.User.AllowGroup != "" {
- if !ls.isUserInGroup(cfg, ldapUser, cfg.User.AllowGroup) {
- return nil, errors.New("user not in allowed group")
- }
- }
- err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
- if err != nil {
- return nil, err
- }
- user, err := ls.mapToLocalUser(cfg, ldapUser)
- if err != nil {
- return nil, errors.Join(ErrLdapToLocalUserFailed, err)
- }
- return user, nil
- }
- // isUserInGroup checks if the user is a member of the specified group. by_sw
- func (ls *LdapService) isUserInGroup(cfg *config.Ldap, ldapUser *LdapUser, groupDN string) bool {
- // Check "memberOf" directly
- if len(ldapUser.MemberOf) > 0 {
- for _, group := range ldapUser.MemberOf {
- if strings.EqualFold(group, groupDN) {
- return true
- }
- }
- }
- // For "member" attribute, perform a reverse search on the group
- member := "member"
- userDN := ldap.EscapeFilter(ldapUser.Dn)
- groupDN = ldap.EscapeFilter(groupDN)
- groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
- // Create the LDAP search request
- groupSearchRequest := ldap.NewSearchRequest(
- groupDN,
- ldap.ScopeWholeSubtree,
- ldap.NeverDerefAliases,
- 0, // Unlimited search results
- 0, // No time limit
- false, // Return both attributes and DN
- groupFilter,
- []string{"dn"},
- nil,
- )
- // Perform the group search
- groupResult, err := ls.searchResult(cfg, groupSearchRequest)
- if err != nil {
- return false
- }
- // If any results are returned, the user is part of the group
- return len(groupResult.Entries) > 0
- }
- // mapToLocalUser checks whether the user exists locally; if not, creates one.
- // If the user exists and Ldap.Sync is enabled, it updates local info.
- func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
- userService := &UserService{}
- localUser := userService.InfoByUsername(lu.Username)
- isAdmin := ls.isUserAdmin(cfg, lu)
- // If the user doesn't exist in local DB, create a new one
- if localUser.Id == 0 {
- newUser := lu.ToUser(nil)
- // Typically, you don’t store LDAP user passwords locally.
- // If needed, you can set a random password here.
- newUser.IsAdmin = &isAdmin
- newUser.GroupId = 1
- if err := DB.Create(newUser).Error; err != nil {
- return nil, errors.Join(ErrLdapCreateUserFailed, err)
- }
- return userService.InfoByUsername(lu.Username), nil
- }
- // If the user already exists and sync is enabled, update local info
- if cfg.User.Sync {
- originalEmail := localUser.Email
- originalNickname := localUser.Nickname
- originalIsAdmin := localUser.IsAdmin
- originalStatus := localUser.Status
- lu.ToUser(localUser) // merges LDAP data into the existing user
- localUser.IsAdmin = &isAdmin
- if err := userService.Update(localUser); err != nil {
- // If the update fails, revert to original data
- localUser.Email = originalEmail
- localUser.Nickname = originalNickname
- localUser.IsAdmin = originalIsAdmin
- localUser.Status = originalStatus
- }
- }
- return localUser, nil
- }
- // IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
- func (ls *LdapService) IsUsernameExists(username string) bool {
- cfg := &Config.Ldap
- if !cfg.Enable {
- return false
- }
- sr, err := ls.usernameSearchResult(cfg, username)
- if err != nil {
- return false
- }
- return len(sr.Entries) > 0
- }
- // IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
- func (ls *LdapService) IsEmailExists(email string) bool {
- cfg := &Config.Ldap
- if !cfg.Enable {
- return false
- }
- sr, err := ls.emailSearchResult(cfg, email)
- if err != nil {
- return false
- }
- return len(sr.Entries) > 0
- }
- // GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
- func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
- cfg := &Config.Ldap
- if !cfg.Enable {
- return nil, ErrLdapNotEnabled
- }
- sr, err := ls.usernameSearchResult(cfg, username)
- if err != nil {
- return nil, errors.Join(ErrLdapSearchFailed, err)
- }
- if len(sr.Entries) != 1 {
- return nil, ErrLdapUserNotFound
- }
- return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
- }
- // 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.
- func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User, error) {
- ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
- if err != nil {
- return &model.User{}, err
- }
- return ls.mapToLocalUser(&Config.Ldap, ldapUser)
- }
- // GetUserInfoByEmailLdap returns the user info from LDAP for the given email.
- func (ls *LdapService) GetUserInfoByEmailLdap(email string) (*LdapUser, error) {
- cfg := &Config.Ldap
- if !cfg.Enable {
- return nil, ErrLdapNotEnabled
- }
- sr, err := ls.emailSearchResult(cfg, email)
- if err != nil {
- return nil, errors.Join(ErrLdapSearchFailed, err)
- }
- if len(sr.Entries) != 1 {
- return nil, ErrLdapUserNotFound
- }
- return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
- }
- // GetUserInfoByEmailLocal returns the user info from LDAP for the given email. if the user exists, it will synchronize the user information to local database.
- func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error) {
- ldapUser, err := ls.GetUserInfoByEmailLdap(email)
- if err != nil {
- return &model.User{}, err
- }
- return ls.mapToLocalUser(&Config.Ldap, ldapUser)
- }
- // usernameSearchResult returns the search result for the given username.
- func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
- // Build the combined filter for the username
- filter := ls.filterField(ls.fieldUsername(cfg), username)
- // Create the *ldap.SearchRequest
- searchRequest := ls.buildUserSearchRequest(cfg, filter)
- return ls.searchResult(cfg, searchRequest)
- }
- // emailSearchResult returns the search result for the given email.
- func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
- filter := ls.filterField(ls.fieldEmail(cfg), email)
- searchRequest := ls.buildUserSearchRequest(cfg, filter)
- return ls.searchResult(cfg, searchRequest)
- }
- func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
- ldapConn, err := ls.connectAndBindAdmin(cfg)
- if err != nil {
- return nil, err
- }
- defer ldapConn.Close()
- return ldapConn.Search(searchRequest)
- }
- // buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
- func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
- baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
- filterConfig := cfg.User.Filter
- if filterConfig == "" {
- filterConfig = "(cn=*)"
- }
- // Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
- combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
- attributes := ls.buildUserAttributes(cfg)
- return ldap.NewSearchRequest(
- baseDn,
- ldap.ScopeWholeSubtree,
- ldap.NeverDerefAliases,
- 0, // unlimited search results
- 0, // no server-side time limit
- false, // typesOnly
- combinedFilter,
- attributes,
- nil,
- )
- }
- // buildUserAttributes returns the list of attributes we want from LDAP user searches.
- func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
- return []string{
- "dn",
- ls.fieldUsername(cfg),
- ls.fieldEmail(cfg),
- ls.fieldFirstName(cfg),
- ls.fieldLastName(cfg),
- ls.fieldMemberOf(),
- ls.fieldUserEnableAttr(cfg),
- }
- }
- // userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
- func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
- lu := &LdapUser{
- Dn: entry.DN,
- Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
- Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
- FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
- LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
- MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
- EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
- }
- // Check if the user is enabled based on the LDAP configuration
- ls.isUserEnabled(cfg, lu)
- return lu
- }
- // filterField helps build simple attribute filters, e.g. (uid=username).
- func (ls *LdapService) filterField(field, value string) string {
- return fmt.Sprintf("(%s=%s)", field, value)
- }
- // fieldUsername returns the configured username attribute or "uid" if not set.
- func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
- if cfg.User.Username == "" {
- return "uid"
- }
- return cfg.User.Username
- }
- // fieldEmail returns the configured email attribute or "mail" if not set.
- func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
- if cfg.User.Email == "" {
- return "mail"
- }
- return cfg.User.Email
- }
- // fieldFirstName returns the configured first name attribute or "givenName" if not set.
- func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
- if cfg.User.FirstName == "" {
- return "givenName"
- }
- return cfg.User.FirstName
- }
- // fieldLastName returns the configured last name attribute or "sn" if not set.
- func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
- if cfg.User.LastName == "" {
- return "sn"
- }
- return cfg.User.LastName
- }
- func (ls *LdapService) fieldMemberOf() string {
- return "memberOf"
- }
- func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
- if cfg.User.EnableAttr == "" {
- return "userAccountControl"
- }
- return cfg.User.EnableAttr
- }
- // baseDnUser returns the user-specific base DN or the global base DN if none is set.
- func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
- if cfg.User.BaseDn == "" {
- return cfg.BaseDn
- }
- return cfg.User.BaseDn
- }
- // isUserAdmin checks if the user is a member of the admin group.
- func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
- // Check if the admin group is configured
- adminGroup := cfg.User.AdminGroup
- if adminGroup == "" {
- return false
- }
- // Check "memberOf" directly
- if len(ldapUser.MemberOf) > 0 {
- for _, group := range ldapUser.MemberOf {
- if strings.EqualFold(group, adminGroup) {
- return true
- }
- }
- return false
- }
- // For "member" attribute, perform a reverse search on the group
- member := "member"
- userDN := ldap.EscapeFilter(ldapUser.Dn)
- adminGroupDn := ldap.EscapeFilter(adminGroup)
- groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
- // Create the LDAP search request
- groupSearchRequest := ldap.NewSearchRequest(
- adminGroupDn,
- ldap.ScopeWholeSubtree,
- ldap.NeverDerefAliases,
- 0, // Unlimited search results
- 0, // No time limit
- false, // Return both attributes and DN
- groupFilter,
- []string{"dn"},
- nil,
- )
- // Perform the group search
- groupResult, err := ls.searchResult(cfg, groupSearchRequest)
- if err != nil {
- return false
- }
- // If any results are returned, the user is part of the admin group
- if len(groupResult.Entries) > 0 {
- return true
- }
- return false
- }
- // isUserEnabled checks if the user is enabled based on the LDAP configuration.
- // If no enable attribute or value is set, all users are considered enabled by default.
- func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
- // Retrieve the enable attribute and expected value from the configuration
- enableAttr := cfg.User.EnableAttr
- enableAttrValue := cfg.User.EnableAttrValue
- // If no enable attribute or value is configured, consider all users as enabled
- if enableAttr == "" || enableAttrValue == "" {
- ldapUser.Enabled = true
- return true
- }
- // Normalize the enable attribute for comparison
- enableAttr = strings.ToLower(enableAttr)
- // Handle Active Directory's userAccountControl attribute
- if enableAttr == "useraccountcontrol" {
- // Parse the userAccountControl value
- userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
- if err != nil {
- fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
- ldapUser.Enabled = false
- return false
- }
- // Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
- const ACCOUNTDISABLE = 0x2
- ldapUser.Enabled = userAccountControl&ACCOUNTDISABLE == 0
- return ldapUser.Enabled
- }
- // For other attributes, perform a direct comparison with the expected value
- ldapUser.Enabled = ldapUser.EnableAttrValue == enableAttrValue
- return ldapUser.Enabled
- }
- // getAttrOfDn retrieves the value of an attribute for a given DN.
- func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
- searchRequest := ldap.NewSearchRequest(
- ldap.EscapeFilter(dn),
- ldap.ScopeBaseObject,
- ldap.NeverDerefAliases,
- 0, // unlimited search results
- 0, // no server-side time limit
- false, // typesOnly
- "(objectClass=*)",
- []string{attr},
- nil,
- )
- sr, err := ls.searchResult(cfg, searchRequest)
- if err != nil {
- return ""
- }
- if len(sr.Entries) == 0 {
- return ""
- }
- return sr.Entries[0].GetAttributeValue(attr)
- }
|