From db6b7db06df5feee87c29000c19a52dbf9a150cc Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Wed, 15 Sep 2021 10:28:37 +0100
Subject: [PATCH] Improve LDAP synchronization efficiency (#16994)

The current LDAP sync routine has order n^2 efficiency. This change reduces this
to order n.log n.

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/source/ldap/source_search.go |  4 +++
 services/auth/source/ldap/source_sync.go   | 42 ++++++++++++----------
 2 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 71db0b770a..9fe2443768 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -26,6 +26,7 @@ type SearchResult struct {
 	SSHPublicKey []string // SSH Public Key
 	IsAdmin      bool     // if user is administrator
 	IsRestricted bool     // if user is restricted
+	LowerName    string   // Lowername
 }
 
 func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -363,6 +364,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 	}
 
 	return &SearchResult{
+		LowerName:    strings.ToLower(username),
 		Username:     username,
 		Name:         firstname,
 		Surname:      surname,
@@ -440,6 +442,8 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
 		if isAttributeSSHPublicKeySet {
 			result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey)
 		}
+		result[i].LowerName = strings.ToLower(result[i].Username)
+
 	}
 
 	return result, nil
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 7e4088e571..f03e29f920 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -7,6 +7,7 @@ package ldap
 import (
 	"context"
 	"fmt"
+	"sort"
 	"strings"
 
 	"code.gitea.io/gitea/models"
@@ -17,7 +18,7 @@ import (
 func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 	log.Trace("Doing: SyncExternalUsers[%s]", source.loginSource.Name)
 
-	var existingUsers []int64
+	var existingUsers []int
 	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 	var sshKeysNeedUpdate bool
 
@@ -34,6 +35,10 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 	default:
 	}
 
+	sort.Slice(users, func(i, j int) bool {
+		return users[i].LowerName < users[j].LowerName
+	})
+
 	sr, err := source.SearchEntries()
 	if err != nil {
 		log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.loginSource.Name)
@@ -48,6 +53,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 		log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
 	}
 
+	sort.Slice(sr, func(i, j int) bool {
+		return sr[i].LowerName < sr[j].LowerName
+	})
+
+	userPos := 0
+
 	for _, su := range sr {
 		select {
 		case <-ctx.Done():
@@ -71,12 +82,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 		}
 
 		var usr *models.User
-		// Search for existing user
-		for _, du := range users {
-			if du.LowerName == strings.ToLower(su.Username) {
-				usr = du
-				break
-			}
+		for userPos < len(users) && users[userPos].LowerName < su.LowerName {
+			userPos++
+		}
+		if userPos < len(users) && users[userPos].LowerName == su.LowerName {
+			usr = users[userPos]
+			existingUsers = append(existingUsers, userPos)
 		}
 
 		fullName := composeFullName(su.Name, su.Surname, su.Username)
@@ -85,7 +96,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			log.Trace("SyncExternalUsers[%s]: Creating user %s", source.loginSource.Name, su.Username)
 
 			usr = &models.User{
-				LowerName:    strings.ToLower(su.Username),
+				LowerName:    su.LowerName,
 				Name:         su.Username,
 				FullName:     fullName,
 				LoginType:    source.loginSource.Type,
@@ -108,8 +119,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 				}
 			}
 		} else if updateExisting {
-			existingUsers = append(existingUsers, usr.ID)
-
 			// Synchronize SSH Public Key if that attribute is set
 			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
 				sshKeysNeedUpdate = true
@@ -161,15 +170,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 
 	// Deactivate users not present in LDAP
 	if updateExisting {
-		for _, usr := range users {
-			found := false
-			for _, uid := range existingUsers {
-				if usr.ID == uid {
-					found = true
-					break
-				}
+		existPos := 0
+		for i, usr := range users {
+			for existPos < len(existingUsers) && i > existingUsers[existPos] {
+				existPos++
 			}
-			if !found {
+			if usr.IsActive && (existPos >= len(existingUsers) || i < existingUsers[existPos]) {
 				log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.loginSource.Name, usr.Name)
 
 				usr.IsActive = false