Просмотр исходного кода

fix: When OIDC and LDAP work togethar (#132 #134)

* fix OIDC create user if LDAP enable

* `newUser.GroupId = 1` for ldap

* fix
Tao Chen месяцев назад: 11
Родитель
Сommit
5224c31d61
2 измененных файлов с 105 добавлено и 27 удалено
  1. 83 22
      service/ldap.go
  2. 22 5
      service/user.go

+ 83 - 22
service/ldap.go

@@ -4,15 +4,30 @@ import (
4
 	"crypto/tls"
4
 	"crypto/tls"
5
 	"errors"
5
 	"errors"
6
 	"fmt"
6
 	"fmt"
7
-	"github.com/go-ldap/ldap/v3"
8
 	"strconv"
7
 	"strconv"
9
 	"strings"
8
 	"strings"
10
 
9
 
10
+	"github.com/go-ldap/ldap/v3"
11
+
11
 	"Gwen/config"
12
 	"Gwen/config"
12
 	"Gwen/global"
13
 	"Gwen/global"
13
 	"Gwen/model"
14
 	"Gwen/model"
14
 )
15
 )
15
 
16
 
17
+var (
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
+)
30
+
16
 // LdapService is responsible for LDAP authentication and user synchronization.
31
 // LdapService is responsible for LDAP authentication and user synchronization.
17
 type LdapService struct {
32
 type LdapService struct {
18
 }
33
 }
@@ -43,6 +58,11 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
43
 	u.Username = lu.Username
58
 	u.Username = lu.Username
44
 	u.Email = lu.Email
59
 	u.Email = lu.Email
45
 	u.Nickname = lu.Name()
60
 	u.Nickname = lu.Name()
61
+	if lu.Enabled {
62
+		u.Status = model.COMMON_STATUS_ENABLE
63
+	} else {
64
+		u.Status = model.COMMON_STATUS_DISABLED
65
+	}
46
 	return u
66
 	return u
47
 }
67
 }
48
 
68
 
@@ -50,21 +70,21 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
50
 func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
70
 func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
51
 	conn, err := ldap.DialURL(cfg.Url)
71
 	conn, err := ldap.DialURL(cfg.Url)
52
 	if err != nil {
72
 	if err != nil {
53
-		return nil, fmt.Errorf("failed to dial LDAP: %w", err)
73
+		return nil, errors.Join(ErrLdapConnectFailed, err)
54
 	}
74
 	}
55
 
75
 
56
 	if cfg.TLS {
76
 	if cfg.TLS {
57
 		// WARNING: InsecureSkipVerify: true is not recommended for production
77
 		// WARNING: InsecureSkipVerify: true is not recommended for production
58
 		if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
78
 		if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
59
 			conn.Close()
79
 			conn.Close()
60
-			return nil, fmt.Errorf("failed to start TLS: %w", err)
80
+			return nil, errors.Join(ErrLdapTlsFailed, err)
61
 		}
81
 		}
62
 	}
82
 	}
63
 
83
 
64
 	// Bind as the "service" user
84
 	// Bind as the "service" user
65
 	if err = conn.Bind(username, password); err != nil {
85
 	if err = conn.Bind(username, password); err != nil {
66
 		conn.Close()
86
 		conn.Close()
67
-		return nil, fmt.Errorf("failed to bind with service account: %w", err)
87
+		return nil, errors.Join(ErrLdapBindService, err)
68
 	}
88
 	}
69
 	return conn, nil
89
 	return conn, nil
70
 }
90
 }
@@ -87,29 +107,17 @@ func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password st
87
 // Authenticate checks the provided username and password against LDAP.
107
 // Authenticate checks the provided username and password against LDAP.
88
 // Returns the corresponding *model.User if successful, or an error if not.
108
 // Returns the corresponding *model.User if successful, or an error if not.
89
 func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
109
 func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
90
-	cfg := &global.Config.Ldap
91
-	// 1. Use a service bind to search for the user DN
92
-	sr, err := ls.usernameSearchResult(cfg, username)
93
-	if err != nil {
94
-		return nil, fmt.Errorf("LDAP search request failed: %w", err)
95
-	}
96
-	if len(sr.Entries) != 1 {
97
-		return nil, errors.New("user does not exist or too many entries returned")
98
-	}
99
-	entry := sr.Entries[0]
100
-	userDN := entry.DN
101
-
102
-	err = ls.verifyCredentials(cfg, userDN, password)
110
+	ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
103
 	if err != nil {
111
 	if err != nil {
104
-		return nil, fmt.Errorf("LDAP authentication failed: %w", err)
112
+		return nil, err
105
 	}
113
 	}
106
-	ldapUser := ls.userResultToLdapUser(cfg, entry)
107
 	if !ldapUser.Enabled {
114
 	if !ldapUser.Enabled {
108
-		return nil, errors.New("UserDisabledAtLdap")
115
+		return nil, ErrLdapUserDisabled
109
 	}
116
 	}
117
+	cfg := &global.Config.Ldap
110
 	user, err := ls.mapToLocalUser(cfg, ldapUser)
118
 	user, err := ls.mapToLocalUser(cfg, ldapUser)
111
 	if err != nil {
119
 	if err != nil {
112
-		return nil, fmt.Errorf("failed to map LDAP user to local user: %w", err)
120
+		return nil, errors.Join(ErrLdapToLocalUserFailed, err)
113
 	}
121
 	}
114
 	return user, nil
122
 	return user, nil
115
 }
123
 }
@@ -126,8 +134,9 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
126
 		// Typically, you don’t store LDAP user passwords locally.
134
 		// Typically, you don’t store LDAP user passwords locally.
127
 		// If needed, you can set a random password here.
135
 		// If needed, you can set a random password here.
128
 		newUser.IsAdmin = &isAdmin
136
 		newUser.IsAdmin = &isAdmin
137
+		newUser.GroupId = 1
129
 		if err := global.DB.Create(newUser).Error; err != nil {
138
 		if err := global.DB.Create(newUser).Error; err != nil {
130
-			return nil, fmt.Errorf("failed to create new user: %w", err)
139
+			return nil, errors.Join(ErrLdapCreateUserFailed, err)
131
 		}
140
 		}
132
 		return userService.InfoByUsername(lu.Username), nil
141
 		return userService.InfoByUsername(lu.Username), nil
133
 	}
142
 	}
@@ -137,6 +146,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
137
 		originalEmail := localUser.Email
146
 		originalEmail := localUser.Email
138
 		originalNickname := localUser.Nickname
147
 		originalNickname := localUser.Nickname
139
 		originalIsAdmin := localUser.IsAdmin
148
 		originalIsAdmin := localUser.IsAdmin
149
+		originalStatus := localUser.Status
140
 		lu.ToUser(localUser) // merges LDAP data into the existing user
150
 		lu.ToUser(localUser) // merges LDAP data into the existing user
141
 		localUser.IsAdmin = &isAdmin
151
 		localUser.IsAdmin = &isAdmin
142
 		if err := userService.Update(localUser); err != nil {
152
 		if err := userService.Update(localUser); err != nil {
@@ -144,6 +154,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
144
 			localUser.Email = originalEmail
154
 			localUser.Email = originalEmail
145
 			localUser.Nickname = originalNickname
155
 			localUser.Nickname = originalNickname
146
 			localUser.IsAdmin = originalIsAdmin
156
 			localUser.IsAdmin = originalIsAdmin
157
+			localUser.Status = originalStatus
147
 		}
158
 		}
148
 	}
159
 	}
149
 
160
 
@@ -177,6 +188,56 @@ func (ls *LdapService) IsEmailExists(email string) bool {
177
 	return len(sr.Entries) > 0
188
 	return len(sr.Entries) > 0
178
 }
189
 }
179
 
190
 
191
+// GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
192
+func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
193
+	cfg := &global.Config.Ldap
194
+	if !cfg.Enable {
195
+		return nil, ErrLdapNotEnabled
196
+	}
197
+	sr, err := ls.usernameSearchResult(cfg, username)
198
+	if err != nil {
199
+		return nil, errors.Join(ErrLdapSearchFailed, err)
200
+	}
201
+	if len(sr.Entries) != 1 {
202
+		return nil, ErrLdapUserNotFound
203
+	}
204
+	return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
205
+}
206
+
207
+// 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.
208
+func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User, error) {
209
+	ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
210
+	if err != nil {
211
+		return &model.User{}, err
212
+	}
213
+	return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
214
+}
215
+
216
+// GetUserInfoByEmailLdap returns the user info from LDAP for the given email.
217
+func (ls *LdapService) GetUserInfoByEmailLdap(email string) (*LdapUser, error) {
218
+	cfg := &global.Config.Ldap
219
+	if !cfg.Enable {
220
+		return nil, ErrLdapNotEnabled
221
+	}
222
+	sr, err := ls.emailSearchResult(cfg, email)
223
+	if err != nil {
224
+		return nil, errors.Join(ErrLdapSearchFailed, err)
225
+	}
226
+	if len(sr.Entries) != 1 {
227
+		return nil, ErrLdapUserNotFound
228
+	}
229
+	return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
230
+}
231
+
232
+// GetUserInfoByEmailLocal returns the user info from LDAP for the given email. if the user exists, it will synchronize the user information to local database.
233
+func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error) {
234
+	ldapUser, err := ls.GetUserInfoByEmailLdap(email)
235
+	if err != nil {
236
+		return &model.User{}, err
237
+	}
238
+	return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
239
+}
240
+
180
 // usernameSearchResult returns the search result for the given username.
241
 // usernameSearchResult returns the search result for the given username.
181
 func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
242
 func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
182
 	// Build the combined filter for the username
243
 	// Build the combined filter for the username

+ 22 - 5
service/user.go

@@ -5,12 +5,13 @@ import (
5
 	"Gwen/model"
5
 	"Gwen/model"
6
 	"Gwen/utils"
6
 	"Gwen/utils"
7
 	"errors"
7
 	"errors"
8
-	"github.com/gin-gonic/gin"
9
-	"gorm.io/gorm"
10
 	"math/rand"
8
 	"math/rand"
11
 	"strconv"
9
 	"strconv"
12
 	"strings"
10
 	"strings"
13
 	"time"
11
 	"time"
12
+
13
+	"github.com/gin-gonic/gin"
14
+	"gorm.io/gorm"
14
 )
15
 )
15
 
16
 
16
 type UserService struct {
17
 type UserService struct {
@@ -322,7 +323,16 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
322
 		email = strings.ToLower(email)
323
 		email = strings.ToLower(email)
323
 		// update email to oauthUser, in case it contain upper case
324
 		// update email to oauthUser, in case it contain upper case
324
 		oauthUser.Email = email
325
 		oauthUser.Email = email
325
-		user := us.InfoByEmail(email)
326
+		// call this, if find user by email, it will update the email to local database
327
+		user, ldapErr := AllService.LdapService.GetUserInfoByEmailLocal(email)
328
+		// If we enable ldap, and the error is not ErrLdapUserNotFound, return the error because we could not sure if the user is not found in ldap
329
+		if !(errors.Is(ldapErr, ErrLdapNotEnabled) || errors.Is(ldapErr, ErrLdapUserNotFound) || ldapErr == nil) {
330
+			return ldapErr, user
331
+		}
332
+		if user.Id == 0 {
333
+			// this means the user is not found in ldap, maybe ldao is not enabled
334
+			user = us.InfoByEmail(email)
335
+		}
326
 		if user.Id != 0 {
336
 		if user.Id != 0 {
327
 			ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
337
 			ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
328
 			global.DB.Create(ut)
338
 			global.DB.Create(ut)
@@ -491,8 +501,15 @@ func (us *UserService) VerifyJWT(token string) (uint, error) {
491
 
501
 
492
 // IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
502
 // IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
493
 func (us *UserService) IsUsernameExists(username string) bool {
503
 func (us *UserService) IsUsernameExists(username string) bool {
504
+	return us.IsUsernameExistsLocal(username) || AllService.LdapService.IsUsernameExists(username)
505
+}
506
+
507
+func (us *UserService) IsUsernameExistsLocal(username string) bool {
494
 	u := &model.User{}
508
 	u := &model.User{}
495
 	global.DB.Where("username = ?", username).First(u)
509
 	global.DB.Where("username = ?", username).First(u)
496
-	existsInLdap := AllService.LdapService.IsUsernameExists(username)
497
-	return u.Id != 0 || existsInLdap
510
+	return u.Id != 0
511
+}
512
+
513
+func (us *UserService) IsEmailExistsLdap(email string) bool {
514
+	return AllService.LdapService.IsEmailExists(email)
498
 }
515
 }