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

feat(ldap): Add LDAP
* rename: Admin to AdminGroup

* update

* cleanup

* tmp save group mapping

* add enableControl(not-test)

* verify username exist before create(for LDAP)

* add getAllGroupsDn()

* rename

* adminGroup

* enable TLS Verify

* init for ldap

---------

Co-authored-by: Tao Chen <iamtaochen@outlook.com>

Tao Chen 1 год назад
Родитель
Сommit
0e3f204327
7 измененных файлов с 519 добавлено и 14 удалено
  1. 20 0
      conf/config.yaml
  2. 1 0
      config/config.go
  3. 36 0
      config/ldap.go
  4. 8 5
      go.mod
  5. 425 0
      service/ldap.go
  6. 1 0
      service/service.go
  7. 28 9
      service/user.go

+ 20 - 0
conf/config.yaml

@@ -58,3 +58,23 @@ oss:
58 58
   expire-time: 30
59 59
   max-byte: 10240
60 60
 
61
+ldap:
62
+  enable: false
63
+  url: "ldap://ldap.example.com:389"
64
+  tls: false
65
+  tls-verify: false
66
+  base-dn: "dc=example,dc=com"
67
+  bind-dn: "cn=admin,dc=example,dc=com"
68
+  bind-password: "password"
69
+
70
+  user:
71
+    base-dn: "ou=users,dc=example,dc=com"
72
+    enable-attr: ""       #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
73
+    enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored. 
74
+    filter: "(cn=*)"
75
+    username: "uid"       # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
76
+    email: "mail"
77
+    first-name: "givenName"
78
+    last-name: "sn"
79
+    sync: false         # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
80
+    admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.

+ 1 - 0
config/config.go

@@ -38,6 +38,7 @@ type Config struct {
38 38
 	Jwt      Jwt
39 39
 	Rustdesk Rustdesk
40 40
 	Proxy    Proxy
41
+	Ldap	 Ldap
41 42
 }
42 43
 
43 44
 // Init 初始化配置

+ 36 - 0
config/ldap.go

@@ -0,0 +1,36 @@
1
+package config
2
+
3
+type LdapUser struct {
4
+	BaseDn 	   		string `mapstructure:"base-dn"` 				// The base DN of the user for searching
5
+	EnableAttr 		string `mapstructure:"enable-attr"` 			// The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
6
+	EnableAttrValue string `mapstructure:"enable-attr-value"` 		// The value of the enable attribute when the user is enabled. If you are using AD, just leave it random str, it will be ignored.
7
+	Filter     		string `mapstructure:"filter"`
8
+	Username   		string `mapstructure:"username"`
9
+	Email      		string `mapstructure:"email"`
10
+	FirstName  		string `mapstructure:"first-name"`
11
+	LastName   		string `mapstructure:"last-name"`
12
+	Sync       		bool   `mapstructure:"sync"` 				// Will sync the user's information to the internal database
13
+	AdminGroup      string `mapstructure:"admin-group"` 	    // Which group is the admin group
14
+}
15
+
16
+// type LdapGroup struct {
17
+// 	BaseDn 		string            `mapstructure:"base-dn"` // The base DN of the group for searching
18
+// 	Name        string            `mapstructure:"name"`    // The attribute name of the group
19
+// 	Filter      string            `mapstructure:"filter"`
20
+// 	Admin       string            `mapstructure:"admin"`   // Which group is the admin group
21
+// 	Member      string            `mapstructure:"member"`  // How to get the member of the group: member, uniqueMember, or memberOf (default: member)
22
+// 	Mode        string            `mapstructure:"mode"`
23
+// 	Map         map[string]string `mapstructure:"map"`     // If mode is "map", map the LDAP group to the internal group
24
+// }
25
+
26
+type Ldap struct {
27
+	Enable       bool      `mapstructure:"enable"`
28
+	Url          string    `mapstructure:"url"`
29
+	TLS          bool      `mapstructure:"tls"`
30
+	TlsVerify 	 bool      `mapstructure:"tls-verify"`
31
+	BaseDn       string    `mapstructure:"base-dn"`
32
+	BindDn       string    `mapstructure:"bind-dn"`
33
+	BindPassword string    `mapstructure:"bind-password"`
34
+	User         LdapUser  `mapstructure:"user"`
35
+	// Group        LdapGroup `mapstructure:"group"`
36
+}

+ 8 - 5
go.mod

@@ -13,7 +13,7 @@ require (
13 13
 	github.com/go-playground/validator/v10 v10.11.2
14 14
 	github.com/go-redis/redis/v8 v8.11.4
15 15
 	github.com/golang-jwt/jwt/v5 v5.2.1
16
-	github.com/google/uuid v1.1.2
16
+	github.com/google/uuid v1.6.0
17 17
 	github.com/nicksnyder/go-i18n/v2 v2.4.0
18 18
 	github.com/sirupsen/logrus v1.8.1
19 19
 	github.com/spf13/cobra v1.8.1
@@ -22,13 +22,14 @@ require (
22 22
 	github.com/swaggo/gin-swagger v1.6.0
23 23
 	github.com/swaggo/swag v1.16.3
24 24
 	golang.org/x/oauth2 v0.23.0
25
-	golang.org/x/text v0.18.0
25
+	golang.org/x/text v0.21.0
26 26
 	gorm.io/driver/mysql v1.5.7
27 27
 	gorm.io/driver/sqlite v1.5.6
28 28
 	gorm.io/gorm v1.25.7
29 29
 )
30 30
 
31 31
 require (
32
+	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
32 33
 	github.com/KyleBanks/depth v1.2.1 // indirect
33 34
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
34 35
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -37,6 +38,8 @@ require (
37 38
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
38 39
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
39 40
 	github.com/gin-contrib/sse v0.1.0 // indirect
41
+	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
42
+	github.com/go-ldap/ldap/v3 v3.4.10 // indirect
40 43
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
41 44
 	github.com/go-openapi/jsonreference v0.19.6 // indirect
42 45
 	github.com/go-openapi/spec v0.20.4 // indirect
@@ -70,10 +73,10 @@ require (
70 73
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
71 74
 	github.com/ugorji/go/codec v1.2.9 // indirect
72 75
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
73
-	golang.org/x/crypto v0.23.0 // indirect
76
+	golang.org/x/crypto v0.31.0 // indirect
74 77
 	golang.org/x/image v0.13.0 // indirect
75
-	golang.org/x/net v0.25.0 // indirect
76
-	golang.org/x/sys v0.25.0 // indirect
78
+	golang.org/x/net v0.33.0 // indirect
79
+	golang.org/x/sys v0.28.0 // indirect
77 80
 	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
78 81
 	google.golang.org/protobuf v1.33.0 // indirect
79 82
 	gopkg.in/ini.v1 v1.63.2 // indirect

+ 425 - 0
service/ldap.go

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

+ 1 - 0
service/service.go

@@ -18,6 +18,7 @@ type Service struct {
18 18
 	*AuditService
19 19
 	*ShareRecordService
20 20
 	*ServerCmdService
21
+	*LdapService
21 22
 }
22 23
 
23 24
 func New() *Service {

+ 28 - 9
service/user.go

@@ -46,6 +46,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
46 46
 
47 47
 // InfoByUsernamePassword 根据用户名密码取用户信息
48 48
 func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
49
+	if global.Config.Ldap.Enable {
50
+		u, err := AllService.LdapService.Authenticate(username, password)
51
+		if err == nil {
52
+			return u
53
+		}
54
+		global.Logger.Error("LDAP authentication failed, %v", err)
55
+		global.Logger.Warn("Fallback to local database")
56
+	}
49 57
 	u := &model.User{}
50 58
 	global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
51 59
 	return u
@@ -156,6 +164,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
156 164
 // Create 创建
157 165
 func (us *UserService) Create(u *model.User) error {
158 166
 	// The initial username should be formatted, and the username should be unique
167
+	if us.IsUsernameExists(u.Username) {
168
+		return errors.New("UsernameExists")
169
+	}
159 170
 	u.Username = us.formatUsername(u.Username)
160 171
 	u.Password = us.EncryptPassword(u.Password)
161 172
 	res := global.DB.Create(u).Error
@@ -343,13 +354,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
343 354
 
344 355
 // GenerateUsernameByOauth 生成用户名
345 356
 func (us *UserService) GenerateUsernameByOauth(name string) string {
346
-	u := &model.User{}
347
-	global.DB.Where("username = ?", name).First(u)
348
-	if u.Id == 0 {
349
-		return name
357
+	for us.IsUsernameExists(name) {
358
+		name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
350 359
 	}
351
-	name = name + strconv.FormatInt(rand.Int63n(10), 10)
352
-	return us.GenerateUsernameByOauth(name)
360
+	return name
353 361
 }
354 362
 
355 363
 // UserThirdsByUserId
@@ -394,15 +402,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
394 402
 	return us.IsPasswordEmptyById(u.Id)
395 403
 }
396 404
 
397
-// Register 注册
405
+// Register 注册, 如果用户名已存在则返回nil
398 406
 func (us *UserService) Register(username string, email string, password string) *model.User {
399 407
 	u := &model.User{
400 408
 		Username: username,
401 409
 		Email:    email,
402
-		Password: us.EncryptPassword(password),
410
+		Password: password,
403 411
 		GroupId:  1,
404 412
 	}
405
-	global.DB.Create(u)
413
+	err := us.Create(u)
414
+	if err != nil {
415
+		return nil
416
+	}
406 417
 	return u
407 418
 }
408 419
 
@@ -468,3 +479,11 @@ func (us *UserService) BatchDeleteUserToken(ids []uint) error {
468 479
 func (us *UserService) VerifyJWT(token string) (uint, error) {
469 480
 	return global.Jwt.ParseToken(token)
470 481
 }
482
+
483
+// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
484
+func (us *UserService) IsUsernameExists(username string) bool {
485
+	u := &model.User{}
486
+	global.DB.Where("username = ?", username).First(u)
487
+	existsInLdap := AllService.LdapService.IsUsernameExists(username)
488
+	return u.Id != 0 || existsInLdap
489
+}