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

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 4
 	"crypto/tls"
5 5
 	"errors"
6 6
 	"fmt"
7
-	"github.com/go-ldap/ldap/v3"
8 7
 	"strconv"
9 8
 	"strings"
10 9
 
10
+	"github.com/go-ldap/ldap/v3"
11
+
11 12
 	"Gwen/config"
12 13
 	"Gwen/global"
13 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 31
 // LdapService is responsible for LDAP authentication and user synchronization.
17 32
 type LdapService struct {
18 33
 }
@@ -43,6 +58,11 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
43 58
 	u.Username = lu.Username
44 59
 	u.Email = lu.Email
45 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 66
 	return u
47 67
 }
48 68
 
@@ -50,21 +70,21 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
50 70
 func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
51 71
 	conn, err := ldap.DialURL(cfg.Url)
52 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 76
 	if cfg.TLS {
57 77
 		// WARNING: InsecureSkipVerify: true is not recommended for production
58 78
 		if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
59 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 84
 	// Bind as the "service" user
65 85
 	if err = conn.Bind(username, password); err != nil {
66 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 89
 	return conn, nil
70 90
 }
@@ -87,29 +107,17 @@ func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password st
87 107
 // Authenticate checks the provided username and password against LDAP.
88 108
 // Returns the corresponding *model.User if successful, or an error if not.
89 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 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 114
 	if !ldapUser.Enabled {
108
-		return nil, errors.New("UserDisabledAtLdap")
115
+		return nil, ErrLdapUserDisabled
109 116
 	}
117
+	cfg := &global.Config.Ldap
110 118
 	user, err := ls.mapToLocalUser(cfg, ldapUser)
111 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 122
 	return user, nil
115 123
 }
@@ -126,8 +134,9 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
126 134
 		// Typically, you don’t store LDAP user passwords locally.
127 135
 		// If needed, you can set a random password here.
128 136
 		newUser.IsAdmin = &isAdmin
137
+		newUser.GroupId = 1
129 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 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 146
 		originalEmail := localUser.Email
138 147
 		originalNickname := localUser.Nickname
139 148
 		originalIsAdmin := localUser.IsAdmin
149
+		originalStatus := localUser.Status
140 150
 		lu.ToUser(localUser) // merges LDAP data into the existing user
141 151
 		localUser.IsAdmin = &isAdmin
142 152
 		if err := userService.Update(localUser); err != nil {
@@ -144,6 +154,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
144 154
 			localUser.Email = originalEmail
145 155
 			localUser.Nickname = originalNickname
146 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 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 241
 // usernameSearchResult returns the search result for the given username.
181 242
 func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
182 243
 	// Build the combined filter for the username

+ 22 - 5
service/user.go

@@ -5,12 +5,13 @@ import (
5 5
 	"Gwen/model"
6 6
 	"Gwen/utils"
7 7
 	"errors"
8
-	"github.com/gin-gonic/gin"
9
-	"gorm.io/gorm"
10 8
 	"math/rand"
11 9
 	"strconv"
12 10
 	"strings"
13 11
 	"time"
12
+
13
+	"github.com/gin-gonic/gin"
14
+	"gorm.io/gorm"
14 15
 )
15 16
 
16 17
 type UserService struct {
@@ -322,7 +323,16 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
322 323
 		email = strings.ToLower(email)
323 324
 		// update email to oauthUser, in case it contain upper case
324 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 336
 		if user.Id != 0 {
327 337
 			ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
328 338
 			global.DB.Create(ut)
@@ -491,8 +501,15 @@ func (us *UserService) VerifyJWT(token string) (uint, error) {
491 501
 
492 502
 // IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
493 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 508
 	u := &model.User{}
495 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
 }