From 834d92a47ba782b0f6cf609799864c4c73d44c5e Mon Sep 17 00:00:00 2001
From: Adam Strzelecki <ono@java.pl>
Date: Tue, 16 Feb 2016 12:33:16 +0100
Subject: [PATCH] LDAP: Fetch attributes in Bind DN context option

This is feature is workaround for #2628 (JumpCloud) and some other services
that allow LDAP search only under BindDN user account, but not allow any LDAP
search query in logged user DN context.

Such approach is an alternative to minimal permissions security pattern for
BindDN user.
---
 conf/locale/locale_en-US.ini   |  1 +
 modules/auth/auth_form.go      |  1 +
 modules/auth/ldap/ldap.go      | 32 ++++++++++++++++++++++++++------
 routers/admin/auths.go         |  1 +
 templates/admin/auth/edit.tmpl |  8 ++++++++
 5 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini
index 5c279c3dfe..fbe70ddb22 100644
--- a/conf/locale/locale_en-US.ini
+++ b/conf/locale/locale_en-US.ini
@@ -921,6 +921,7 @@ auths.attribute_username_placeholder = Leave empty to use sign-in form field val
 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.filter = User Filter
 auths.admin_filter = Admin Filter
 auths.ms_ad_sa = Ms Ad SA
diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go
index 68a9688303..15dbb3605b 100644
--- a/modules/auth/auth_form.go
+++ b/modules/auth/auth_form.go
@@ -23,6 +23,7 @@ type AuthenticationForm struct {
 	AttributeName     string
 	AttributeSurname  string
 	AttributeMail     string
+	AttributesInBind  bool
 	Filter            string
 	AdminFilter       string
 	IsActive          bool
diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go
index 8fbefb4341..376092fcb0 100644
--- a/modules/auth/ldap/ldap.go
+++ b/modules/auth/ldap/ldap.go
@@ -31,6 +31,7 @@ type Source struct {
 	AttributeName     string // First name attribute
 	AttributeSurname  string // Surname attribute
 	AttributeMail     string // E-mail attribute
+	AttributesInBind  bool   // fetch attributes in bind context (not user)
 	Filter            string // Query filter to validate entry
 	AdminFilter       string // Query filter to check if user is admin
 	Enabled           bool   // if this source is disabled
@@ -130,14 +131,14 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 	}
 
-	log.Trace("Binding with userDN: %s", userDN)
-	err = l.Bind(userDN, passwd)
-	if err != nil {
-		log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
-		return "", "", "", "", false, false
+	if directBind || !ls.AttributesInBind {
+		// binds user (checking password) before looking-up attributes in user context
+		err = bindUser(l, userDN, passwd)
+		if err != nil {
+			return "", "", "", "", false, false
+		}
 	}
 
-	log.Trace("Bound successfully with userDN: %s", userDN)
 	userFilter, ok := ls.sanitizedUserQuery(name)
 	if !ok {
 		return "", "", "", "", false, false
@@ -184,9 +185,28 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 	}
 
+	if !directBind && ls.AttributesInBind {
+		// binds user (checking password) after looking-up attributes in BindDN context
+		err = bindUser(l, userDN, passwd)
+		if err != nil {
+			return "", "", "", "", false, false
+		}
+	}
+
 	return username_attr, name_attr, sn_attr, mail_attr, admin_attr, true
 }
 
+func bindUser(l *ldap.Conn, userDN, passwd string) error {
+	log.Trace("Binding with userDN: %s", userDN)
+	err := l.Bind(userDN, passwd)
+	if err != nil {
+		log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
+		return err
+	}
+	log.Trace("Bound successfully with userDN: %s", userDN)
+	return err
+}
+
 func ldapDial(ls *Source) (*ldap.Conn, error) {
 	if ls.UseSSL {
 		log.Debug("Using TLS for LDAP without verifying: %v", ls.SkipVerify)
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
index 659b8fcf67..c519d5a7e0 100644
--- a/routers/admin/auths.go
+++ b/routers/admin/auths.go
@@ -81,6 +81,7 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
 			AttributeName:     form.AttributeName,
 			AttributeSurname:  form.AttributeSurname,
 			AttributeMail:     form.AttributeMail,
+			AttributesInBind:  form.AttributesInBind,
 			Filter:            form.Filter,
 			AdminFilter:       form.AdminFilter,
 			Enabled:           true,
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 426d37e40a..9bda877980 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -79,6 +79,14 @@
 								<label for="attribute_mail">{{.i18n.Tr "admin.auths.attribute_mail"}}</label>
 								<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><strong>{{.i18n.Tr "admin.auths.attributes_in_bind"}}</strong></label>
+										<input name="attributes_in_bind" type="checkbox" {{if $cfg.AttributesInBind}}checked{{end}}>
+									</div>
+								</div>
+							{{end}}
 						{{end}}
 
 						<!-- SMTP -->