From 501fb228e6c2b4d75593fae835d59cc533a6f36b Mon Sep 17 00:00:00 2001
From: Lauris BH <lauris@nix.lv>
Date: Sat, 5 May 2018 17:30:47 +0300
Subject: [PATCH] Add option to use paged LDAP search when synchronizing users
 (#3895)

---
 modules/auth/auth_form.go             |  2 ++
 modules/auth/ldap/ldap.go             | 13 ++++++++++++-
 options/locale/locale_en-US.ini       |  2 ++
 public/js/index.js                    | 19 ++++++++++++++++++-
 routers/admin/auths.go                |  5 +++++
 templates/admin/auth/edit.tmpl        | 10 ++++++++++
 templates/admin/auth/source/ldap.tmpl | 10 ++++++++++
 7 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go
index 7c452bbc35..8fe07d0737 100644
--- a/modules/auth/auth_form.go
+++ b/modules/auth/auth_form.go
@@ -25,6 +25,8 @@ type AuthenticationForm struct {
 	AttributeSurname              string
 	AttributeMail                 string
 	AttributesInBind              bool
+	UsePagedSearch                bool
+	SearchPageSize                int
 	Filter                        string
 	AdminFilter                   string
 	IsActive                      bool
diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go
index bb69f35587..2e2db004f6 100644
--- a/modules/auth/ldap/ldap.go
+++ b/modules/auth/ldap/ldap.go
@@ -42,6 +42,7 @@ type Source struct {
 	AttributeSurname  string // Surname attribute
 	AttributeMail     string // E-mail attribute
 	AttributesInBind  bool   // fetch attributes in bind context (not user)
+	SearchPageSize    uint32 // Search with paging page size
 	Filter            string // Query filter to validate entry
 	AdminFilter       string // Query filter to check if user is admin
 	Enabled           bool   // if this source is disabled
@@ -269,6 +270,11 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 	}
 }
 
+// UsePagedSearch returns if need to use paged search
+func (ls *Source) UsePagedSearch() bool {
+	return ls.SearchPageSize > 0
+}
+
 // SearchEntries : search an LDAP source for all users matching userFilter
 func (ls *Source) SearchEntries() []*SearchResult {
 	l, err := dial(ls)
@@ -298,7 +304,12 @@ func (ls *Source) SearchEntries() []*SearchResult {
 		[]string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},
 		nil)
 
-	sr, err := l.Search(search)
+	var sr *ldap.SearchResult
+	if ls.UsePagedSearch() {
+		sr, err = l.SearchWithPaging(search, ls.SearchPageSize)
+	} else {
+		sr, err = l.Search(search)
+	}
 	if err != nil {
 		log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
 		return nil
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 379da8aabc..0e274fab0b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1352,6 +1352,8 @@ auths.attribute_name = First Name Attribute
 auths.attribute_surname = Surname Attribute
 auths.attribute_mail = Email Attribute
 auths.attributes_in_bind = Fetch Attributes in Bind DN Context
+auths.use_paged_search = Use paged search
+auths.search_page_size = Page size
 auths.filter = User Filter
 auths.admin_filter = Admin Filter
 auths.ms_ad_sa = MS AD Search Attributes
diff --git a/public/js/index.js b/public/js/index.js
index f1d308457e..dc473b6f3b 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1138,6 +1138,16 @@ function initAdmin() {
         }
     }
 
+    function onUsePagedSearchChange() {
+        if ($('#use_paged_search').prop('checked')) {
+            $('.search-page-size').show()
+                .find('input').attr('required', 'required');
+        } else {
+            $('.search-page-size').hide()
+                .find('input').removeAttr('required');
+        }
+    }
+
     function onOAuth2Change() {
         $('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url').hide();
         $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required');
@@ -1191,7 +1201,7 @@ function initAdmin() {
     // New authentication
     if ($('.admin.new.authentication').length > 0) {
         $('#auth_type').change(function () {
-            $('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls').hide();
+            $('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls .search-page-size').hide();
 
             $('.ldap input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required]').removeAttr('required');
 
@@ -1223,9 +1233,13 @@ function initAdmin() {
             if (authType == '2' || authType == '5') {
                 onSecurityProtocolChange()
             }
+            if (authType == '2') {
+                onUsePagedSearchChange();
+            }
         });
         $('#auth_type').change();
         $('#security_protocol').change(onSecurityProtocolChange);
+        $('#use_paged_search').change(onUsePagedSearchChange);
         $('#oauth2_provider').change(onOAuth2Change);
         $('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange);
     }
@@ -1234,6 +1248,9 @@ function initAdmin() {
         var authType = $('#auth_type').val();
         if (authType == '2' || authType == '5') {
             $('#security_protocol').change(onSecurityProtocolChange);
+            if (authType == '2') {
+                $('#use_paged_search').change(onUsePagedSearchChange);
+            }
         } else if (authType == '6') {
             $('#oauth2_provider').change(onOAuth2Change);
             $('#oauth2_use_custom_url').change(onOAuth2UseCustomURLChange);
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
index 3915c618b3..6f142d7975 100644
--- a/routers/admin/auths.go
+++ b/routers/admin/auths.go
@@ -91,6 +91,10 @@ func NewAuthSource(ctx *context.Context) {
 }
 
 func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
+	var pageSize uint32
+	if form.UsePagedSearch {
+		pageSize = uint32(form.SearchPageSize)
+	}
 	return &models.LDAPConfig{
 		Source: &ldap.Source{
 			Name:              form.Name,
@@ -107,6 +111,7 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
 			AttributeSurname:  form.AttributeSurname,
 			AttributeMail:     form.AttributeMail,
 			AttributesInBind:  form.AttributesInBind,
+			SearchPageSize:    pageSize,
 			Filter:            form.Filter,
 			AdminFilter:       form.AdminFilter,
 			Enabled:           true,
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index e3048b2183..e4ec3a9f5b 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -91,6 +91,16 @@
 						<input id="attribute_mail" name="attribute_mail" value="{{$cfg.AttributeMail}}" placeholder="e.g. mail" required>
 					</div>
 					{{if .Source.IsLDAP}}
+						<div class="inline field">
+							<div class="ui checkbox">
+								<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>
+								<input id="use_paged_search" name="use_paged_search" type="checkbox" {{if $cfg.UsePagedSearch}}checked{{end}}>
+							</div>
+						</div>
+						<div class="field required search-page-size{{if not $cfg.UsePagedSearch}} hide{{end}}">
+							<label for="search_page_size">{{.i18n.Tr "admin.auths.search_page_size"}}</label>
+							<input id="search_page_size" name="search_page_size" value="{{if $cfg.UsePagedSearch}}{{$cfg.SearchPageSize}}{{end}}">
+						</div>
 						<div class="inline field">
 							<div class="ui checkbox">
 								<label><strong>{{.i18n.Tr "admin.auths.attributes_in_bind"}}</strong></label>
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index 2131950212..cf906f703c 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -62,4 +62,14 @@
 		<label for="attribute_mail">{{.i18n.Tr "admin.auths.attribute_mail"}}</label>
 		<input id="attribute_mail" name="attribute_mail" value="{{.attribute_mail}}" placeholder="e.g. mail">
 	</div>
+	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
+		<div class="ui checkbox">
+			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>
+			<input id="use_paged_search" name="use_paged_search" class="use-paged-search" type="checkbox" {{if .use_paged_search}}checked{{end}}>
+		</div>
+	</div>
+	<div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}hide{{end}}">
+		<label for="search_page_size">{{.i18n.Tr "admin.auths.search_page_size"}}</label>
+		<input id="search_page_size" name="search_page_size" value="{{.search_page_size}}">
+	</div>
 </div>