Skip to content

Commit dfefe86

Browse files
GiteaBotZettat123
andauthored
Fix LDAP sync when Username Attribute is empty (#25278) (#25379)
Backport #25278 by @Zettat123 Fix #21072 ![image](https://github.com/go-gitea/gitea/assets/15528715/96b30beb-7f88-4a60-baae-2e5ad8049555) Username Attribute is not a required item when creating an authentication source. If Username Attribute is empty, the username value of LDAP user cannot be read, so all users from LDAP will be marked as inactive by mistake when synchronizing external users. This PR improves the sync logic, if username is empty, the email address will be used to find user. Co-authored-by: Zettat123 <[email protected]>
1 parent 10fcb55 commit dfefe86

File tree

2 files changed

+83
-30
lines changed

2 files changed

+83
-30
lines changed

services/auth/source/ldap/source_sync.go

+32-30
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package ldap
66
import (
77
"context"
88
"fmt"
9-
"sort"
109
"strings"
1110

1211
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -24,7 +23,6 @@ import (
2423
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
2524
log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name)
2625

27-
var existingUsers []int
2826
isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
2927
var sshKeysNeedUpdate bool
3028

@@ -41,9 +39,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
4139
default:
4240
}
4341

44-
sort.Slice(users, func(i, j int) bool {
45-
return users[i].LowerName < users[j].LowerName
46-
})
42+
usernameUsers := make(map[string]*user_model.User, len(users))
43+
mailUsers := make(map[string]*user_model.User, len(users))
44+
keepActiveUsers := make(map[int64]struct{})
45+
46+
for _, u := range users {
47+
usernameUsers[u.LowerName] = u
48+
mailUsers[strings.ToLower(u.Email)] = u
49+
}
4750

4851
sr, err := source.SearchEntries()
4952
if err != nil {
@@ -59,11 +62,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
5962
log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
6063
}
6164

62-
sort.Slice(sr, func(i, j int) bool {
63-
return sr[i].LowerName < sr[j].LowerName
64-
})
65-
66-
userPos := 0
6765
orgCache := make(map[string]*organization.Organization)
6866
teamCache := make(map[string]*organization.Team)
6967

@@ -86,21 +84,27 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
8684
return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name)
8785
default:
8886
}
89-
if len(su.Username) == 0 {
87+
if len(su.Username) == 0 && len(su.Mail) == 0 {
9088
continue
9189
}
9290

93-
if len(su.Mail) == 0 {
94-
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
91+
var usr *user_model.User
92+
if len(su.Username) > 0 {
93+
usr = usernameUsers[su.LowerName]
94+
}
95+
if usr == nil && len(su.Mail) > 0 {
96+
usr = mailUsers[strings.ToLower(su.Mail)]
9597
}
9698

97-
var usr *user_model.User
98-
for userPos < len(users) && users[userPos].LowerName < su.LowerName {
99-
userPos++
99+
if usr != nil {
100+
keepActiveUsers[usr.ID] = struct{}{}
101+
} else if len(su.Username) == 0 {
102+
// we cannot create the user if su.Username is empty
103+
continue
100104
}
101-
if userPos < len(users) && users[userPos].LowerName == su.LowerName {
102-
usr = users[userPos]
103-
existingUsers = append(existingUsers, userPos)
105+
106+
if len(su.Mail) == 0 {
107+
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
104108
}
105109

106110
fullName := composeFullName(su.Name, su.Surname, su.Username)
@@ -203,19 +207,17 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
203207

204208
// Deactivate users not present in LDAP
205209
if updateExisting {
206-
existPos := 0
207-
for i, usr := range users {
208-
for existPos < len(existingUsers) && i > existingUsers[existPos] {
209-
existPos++
210+
for _, usr := range users {
211+
if _, ok := keepActiveUsers[usr.ID]; ok {
212+
continue
210213
}
211-
if usr.IsActive && (existPos >= len(existingUsers) || i < existingUsers[existPos]) {
212-
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
213214

214-
usr.IsActive = false
215-
err = user_model.UpdateUserCols(ctx, usr, "is_active")
216-
if err != nil {
217-
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
218-
}
215+
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
216+
217+
usr.IsActive = false
218+
err = user_model.UpdateUserCols(ctx, usr, "is_active")
219+
if err != nil {
220+
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
219221
}
220222
}
221223
}

tests/integration/auth_ldap_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,57 @@ func TestLDAPUserSync(t *testing.T) {
268268
}
269269
}
270270

271+
func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
272+
if skipLDAPTests() {
273+
t.Skip()
274+
return
275+
}
276+
defer tests.PrepareTestEnv(t)()
277+
278+
session := loginUser(t, "user1")
279+
csrf := GetCSRF(t, session, "/admin/auths/new")
280+
payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "")
281+
payload["attribute_username"] = ""
282+
req := NewRequestWithValues(t, "POST", "/admin/auths/new", payload)
283+
session.MakeRequest(t, req, http.StatusSeeOther)
284+
285+
for _, u := range gitLDAPUsers {
286+
req := NewRequest(t, "GET", "/admin/users?q="+u.UserName)
287+
resp := session.MakeRequest(t, req, http.StatusOK)
288+
289+
htmlDoc := NewHTMLParser(t, resp.Body)
290+
291+
tr := htmlDoc.doc.Find("table.table tbody tr")
292+
assert.True(t, tr.Length() == 0)
293+
}
294+
295+
for _, u := range gitLDAPUsers {
296+
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
297+
"_csrf": csrf,
298+
"user_name": u.UserName,
299+
"password": u.Password,
300+
})
301+
MakeRequest(t, req, http.StatusSeeOther)
302+
}
303+
304+
auth.SyncExternalUsers(context.Background(), true)
305+
306+
authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
307+
Name: payload["name"],
308+
})
309+
unittest.AssertCount(t, &user_model.User{
310+
LoginType: auth_model.LDAP,
311+
LoginSource: authSource.ID,
312+
}, len(gitLDAPUsers))
313+
314+
for _, u := range gitLDAPUsers {
315+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
316+
Name: u.UserName,
317+
})
318+
assert.True(t, user.IsActive)
319+
}
320+
}
321+
271322
func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
272323
if skipLDAPTests() {
273324
t.Skip()

0 commit comments

Comments
 (0)