]> source.dussan.org Git - gitblit.git/commitdiff
Extract LdapConnection into new class from LdapAuthProvider
authorFlorian Zschocke <florian.zschocke@devolo.de>
Wed, 23 Nov 2016 01:59:39 +0000 (02:59 +0100)
committerFlorian Zschocke <florian.zschocke@devolo.de>
Wed, 23 Nov 2016 01:59:39 +0000 (02:59 +0100)
Extract the inner class `LdapConnection` from the `LdapAuthProvider`
into a separate class, so that it can be used from multiple classes
that have to connect to an LDAP directory.
The new class is placed into the new package `com.gitblit.ldap`, since
it isn't specific to authentication.

src/main/java/com/gitblit/auth/LdapAuthProvider.java
src/main/java/com/gitblit/ldap/LdapConnection.java [new file with mode: 0644]
src/test/java/com/gitblit/tests/LdapConnectionTest.java [new file with mode: 0644]

index 19fd46325fdd781981d625a5ac3dce6c9495bddf..8a326cdc4d01ade33616ec92e614e4ac1e9f00c8 100644 (file)
@@ -16,9 +16,6 @@
  */
 package com.gitblit.auth;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.GeneralSecurityException;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -33,28 +30,20 @@ import com.gitblit.Constants.AccountType;
 import com.gitblit.Constants.Role;
 import com.gitblit.Keys;
 import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
+import com.gitblit.ldap.LdapConnection;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.service.LdapSyncService;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 import com.unboundid.ldap.sdk.Attribute;
-import com.unboundid.ldap.sdk.BindRequest;
 import com.unboundid.ldap.sdk.BindResult;
-import com.unboundid.ldap.sdk.DereferencePolicy;
-import com.unboundid.ldap.sdk.ExtendedResult;
-import com.unboundid.ldap.sdk.LDAPConnection;
 import com.unboundid.ldap.sdk.LDAPException;
-import com.unboundid.ldap.sdk.LDAPSearchException;
 import com.unboundid.ldap.sdk.ResultCode;
 import com.unboundid.ldap.sdk.SearchRequest;
 import com.unboundid.ldap.sdk.SearchResult;
 import com.unboundid.ldap.sdk.SearchResultEntry;
 import com.unboundid.ldap.sdk.SearchScope;
-import com.unboundid.ldap.sdk.SimpleBindRequest;
-import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
-import com.unboundid.util.ssl.SSLUtil;
-import com.unboundid.util.ssl.TrustAllTrustManager;
 
 /**
  * Implementation of an LDAP user service.
@@ -109,7 +98,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
                if (enabled) {
                        logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
                        final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
-                       LdapConnection ldapConnection = new LdapConnection();
+                       LdapConnection ldapConnection = new LdapConnection(settings);
                        if (ldapConnection.connect()) {
                                if (ldapConnection.bind() == null) {
                                        ldapConnection.close();
@@ -265,7 +254,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
        public UserModel authenticate(String username, char[] password) {
                String simpleUsername = getSimpleUsername(username);
 
-               LdapConnection ldapConnection = new LdapConnection();
+               LdapConnection ldapConnection = new LdapConnection(settings);
                if (ldapConnection.connect()) {
 
                        // Try to bind either to the "manager" account,
@@ -288,7 +277,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
                                // Find the logging in user's DN
                                String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
                                String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
-                               accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+                               accountPattern = StringUtils.replace(accountPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));
 
                                SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
                                if (result != null && result.getEntryCount() == 1) {
@@ -441,12 +430,12 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
                String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
                String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
 
-               groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
-               groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+               groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", LdapConnection.escapeLDAPSearchFilter(loggingInUserDN));
+               groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));
 
                // Fill in attributes into groupMemberPattern
                for (Attribute userAttribute : loggingInUser.getAttributes()) {
-                       groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
+                       groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", LdapConnection.escapeLDAPSearchFilter(userAttribute.getValue()));
                }
 
                SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
@@ -553,34 +542,6 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
                return username;
        }
 
-       // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
-       private static final String escapeLDAPSearchFilter(String filter) {
-               StringBuilder sb = new StringBuilder();
-               for (int i = 0; i < filter.length(); i++) {
-                       char curChar = filter.charAt(i);
-                       switch (curChar) {
-                       case '\\':
-                               sb.append("\\5c");
-                               break;
-                       case '*':
-                               sb.append("\\2a");
-                               break;
-                       case '(':
-                               sb.append("\\28");
-                               break;
-                       case ')':
-                               sb.append("\\29");
-                               break;
-                       case '\u0000':
-                               sb.append("\\00");
-                               break;
-                       default:
-                               sb.append(curChar);
-                       }
-               }
-               return sb.toString();
-       }
-
        private void configureSyncService() {
                LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
                if (ldapSyncService.isReady()) {
@@ -593,226 +554,4 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
                        logger.info("Ldap sync service is disabled.");
                }
        }
-
-
-
-       private class LdapConnection {
-               private LDAPConnection conn;
-               private SimpleBindRequest currentBindRequest;
-               private SimpleBindRequest managerBindRequest;
-               private SimpleBindRequest userBindRequest;
-
-
-               public LdapConnection() {
-                       String bindUserName = settings.getString(Keys.realm.ldap.username, "");
-                       String bindPassword = settings.getString(Keys.realm.ldap.password, "");
-                       if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
-                               this.managerBindRequest = new SimpleBindRequest();
-                       }
-                       this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
-               }
-
-
-               boolean connect() {
-                       try {
-                               URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
-                               String ldapHost = ldapUrl.getHost();
-                               int ldapPort = ldapUrl.getPort();
-
-                               if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
-                                       // SSL
-                                       SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
-                                       conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
-                                       if (ldapPort == -1) {
-                                               ldapPort = 636;
-                                       }
-                               } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
-                                       // no encryption or StartTLS
-                                       conn = new LDAPConnection();
-                                        if (ldapPort == -1) {
-                                                ldapPort = 389;
-                                        }
-                               } else {
-                                       logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
-                                       return false;
-                               }
-
-                               conn.connect(ldapHost, ldapPort);
-
-                               if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
-                                       SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
-                                       ExtendedResult extendedResult = conn.processExtendedOperation(
-                                                       new StartTLSExtendedRequest(sslUtil.createSSLContext()));
-                                       if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
-                                               throw new LDAPException(extendedResult.getResultCode());
-                                       }
-                               }
-
-                               return true;
-
-                       } catch (URISyntaxException e) {
-                               logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
-                       } catch (GeneralSecurityException e) {
-                               logger.error("Unable to create SSL Connection", e);
-                       } catch (LDAPException e) {
-                               logger.error("Error Connecting to LDAP", e);
-                       }
-
-                       return false;
-               }
-
-
-               void close() {
-                       if (conn != null) {
-                               conn.close();
-                       }
-               }
-
-
-               SearchResult search(SearchRequest request) {
-                       try {
-                               return conn.search(request);
-                       } catch (LDAPSearchException e) {
-                               logger.error("Problem Searching LDAP [{}]",  e.getResultCode());
-                               return e.getSearchResult();
-                       }
-               }
-
-
-               SearchResult search(String base, boolean dereferenceAliases, String filter, List<String> attributes) {
-                       try {
-                               SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
-                               if (dereferenceAliases) {
-                                       searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
-                               }
-                               if (attributes != null) {
-                                       searchRequest.setAttributes(attributes);
-                               }
-                               SearchResult result = search(searchRequest);
-                               return result;
-
-                       } catch (LDAPException e) {
-                               logger.error("Problem creating LDAP search", e);
-                               return null;
-                       }
-               }
-
-
-
-               /**
-                * Bind using the manager credentials set in realm.ldap.username and ..password
-                * @return A bind result, or null if binding failed.
-                */
-               BindResult bind() {
-                       BindResult result = null;
-                       try {
-                               result = conn.bind(managerBindRequest);
-                               currentBindRequest = managerBindRequest;
-                       } catch (LDAPException e) {
-                               logger.error("Error authenticating to LDAP with manager account to search the directory.");
-                               logger.error("  Please check your settings for realm.ldap.username and realm.ldap.password.");
-                               logger.debug("  Received exception when binding to LDAP", e);
-                               return null;
-                       }
-                       return result;
-               }
-
-
-               /**
-                * Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
-                * create the DN.
-                * @return A bind result, or null if binding failed.
-                */
-               BindResult bind(String bindPattern, String simpleUsername, String password) {
-                       BindResult result = null;
-                       try {
-                               String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
-                               SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
-                               result = conn.bind(request);
-                               userBindRequest = request;
-                               currentBindRequest = userBindRequest;
-                       } catch (LDAPException e) {
-                               logger.error("Error authenticating to LDAP with user account to search the directory.");
-                               logger.error("  Please check your settings for realm.ldap.bindpattern.");
-                               logger.debug("  Received exception when binding to LDAP", e);
-                               return null;
-                       }
-                       return result;
-               }
-
-
-               boolean rebindAsUser() {
-                       if (userBindRequest == null || currentBindRequest == userBindRequest) {
-                               return false;
-                       }
-                       try {
-                               conn.bind(userBindRequest);
-                               currentBindRequest = userBindRequest;
-                       } catch (LDAPException e) {
-                               conn.close();
-                               logger.error("Error rebinding to LDAP with user account.", e);
-                               return false;
-                       }
-                       return true;
-               }
-
-
-               boolean isAuthenticated(String userDn, String password) {
-                       verifyCurrentBinding();
-
-                       // If the currently bound DN is already the DN of the logging in user, authentication has already happened
-                       // during the previous bind operation. We accept this and return with the current bind left in place.
-                       // This could also be changed to always retry binding as the logging in user, to make sure that the
-                       // connection binding has not been tampered with in between. So far I see no way how this could happen
-                       // and thus skip the repeated binding.
-                       // This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
-                       // when searching the user entry.
-                       String boundDN = currentBindRequest.getBindDN();
-                       if (boundDN != null && boundDN.equals(userDn)) {
-                               return true;
-                       }
-
-                       // Bind a the logging in user to check for authentication.
-                       // Afterwards, bind as the original bound DN again, to restore the previous authorization.
-                       boolean isAuthenticated = false;
-                       try {
-                               // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
-                               SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
-                               conn.bind(ubr);
-                               isAuthenticated = true;
-                               userBindRequest = ubr;
-                       } catch (LDAPException e) {
-                               logger.error("Error authenticating user ({})", userDn, e);
-                       }
-
-                       try {
-                               conn.bind(currentBindRequest);
-                       } catch (LDAPException e) {
-                               logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
-                                                       e.getResultCode(), e);
-                       }
-                       return isAuthenticated;
-               }
-
-
-
-               private boolean verifyCurrentBinding() {
-                       BindRequest lastBind = conn.getLastBindRequest();
-                       if (lastBind == currentBindRequest) {
-                               return true;
-                       }
-                       logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);
-
-                       String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
-                       String boundDN = currentBindRequest.getBindDN();
-                       logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
-                       if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
-                               logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
-                               logger.warn("Updated binding information in LDAP connection.");
-                               currentBindRequest = (SimpleBindRequest)lastBind;
-                               return false;
-                       }
-                       return true;
-               }
-       }
 }
diff --git a/src/main/java/com/gitblit/ldap/LdapConnection.java b/src/main/java/com/gitblit/ldap/LdapConnection.java
new file mode 100644 (file)
index 0000000..b7f07a1
--- /dev/null
@@ -0,0 +1,288 @@
+package com.gitblit.ldap;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.utils.StringUtils;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.DereferencePolicy;
+import com.unboundid.ldap.sdk.ExtendedResult;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchRequest;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.SimpleBindRequest;
+import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+
+public class LdapConnection implements AutoCloseable {
+
+       private final Logger logger = LoggerFactory.getLogger(getClass());
+
+       private IStoredSettings settings;
+
+       private LDAPConnection conn;
+       private SimpleBindRequest currentBindRequest;
+       private SimpleBindRequest managerBindRequest;
+       private SimpleBindRequest userBindRequest;
+
+
+       // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
+       public static final String escapeLDAPSearchFilter(String filter) {
+               StringBuilder sb = new StringBuilder();
+               for (int i = 0; i < filter.length(); i++) {
+                       char curChar = filter.charAt(i);
+                       switch (curChar) {
+                       case '\\':
+                               sb.append("\\5c");
+                               break;
+                       case '*':
+                               sb.append("\\2a");
+                               break;
+                       case '(':
+                               sb.append("\\28");
+                               break;
+                       case ')':
+                               sb.append("\\29");
+                               break;
+                       case '\u0000':
+                               sb.append("\\00");
+                               break;
+                       default:
+                               sb.append(curChar);
+                       }
+               }
+               return sb.toString();
+       }
+
+
+
+       public LdapConnection(IStoredSettings settings) {
+               this.settings = settings;
+
+               String bindUserName = settings.getString(Keys.realm.ldap.username, "");
+               String bindPassword = settings.getString(Keys.realm.ldap.password, "");
+               if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
+                       this.managerBindRequest = new SimpleBindRequest();
+               }
+               this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
+       }
+
+
+
+       public boolean connect() {
+               try {
+                       URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
+                       String ldapHost = ldapUrl.getHost();
+                       int ldapPort = ldapUrl.getPort();
+
+                       if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
+                               // SSL
+                               SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+                               conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
+                               if (ldapPort == -1) {
+                                       ldapPort = 636;
+                               }
+                       } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+                               // no encryption or StartTLS
+                               conn = new LDAPConnection();
+                                if (ldapPort == -1) {
+                                        ldapPort = 389;
+                                }
+                       } else {
+                               logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
+                               return false;
+                       }
+
+                       conn.connect(ldapHost, ldapPort);
+
+                       if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+                               SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+                               ExtendedResult extendedResult = conn.processExtendedOperation(
+                                               new StartTLSExtendedRequest(sslUtil.createSSLContext()));
+                               if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
+                                       throw new LDAPException(extendedResult.getResultCode());
+                               }
+                       }
+
+                       return true;
+
+               } catch (URISyntaxException e) {
+                       logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
+               } catch (GeneralSecurityException e) {
+                       logger.error("Unable to create SSL Connection", e);
+               } catch (LDAPException e) {
+                       logger.error("Error Connecting to LDAP", e);
+               }
+
+               return false;
+       }
+
+
+       public void close() {
+               if (conn != null) {
+                       conn.close();
+               }
+       }
+
+
+
+       /**
+        * Bind using the manager credentials set in realm.ldap.username and ..password
+        * @return A bind result, or null if binding failed.
+        */
+       public BindResult bind() {
+               BindResult result = null;
+               try {
+                       result = conn.bind(managerBindRequest);
+                       currentBindRequest = managerBindRequest;
+               } catch (LDAPException e) {
+                       logger.error("Error authenticating to LDAP with manager account to search the directory.");
+                       logger.error("  Please check your settings for realm.ldap.username and realm.ldap.password.");
+                       logger.debug("  Received exception when binding to LDAP", e);
+                       return null;
+               }
+               return result;
+       }
+
+
+       /**
+        * Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
+        * create the DN.
+        * @return A bind result, or null if binding failed.
+        */
+       public BindResult bind(String bindPattern, String simpleUsername, String password) {
+               BindResult result = null;
+               try {
+                       String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+                       SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
+                       result = conn.bind(request);
+                       userBindRequest = request;
+                       currentBindRequest = userBindRequest;
+               } catch (LDAPException e) {
+                       logger.error("Error authenticating to LDAP with user account to search the directory.");
+                       logger.error("  Please check your settings for realm.ldap.bindpattern.");
+                       logger.debug("  Received exception when binding to LDAP", e);
+                       return null;
+               }
+               return result;
+       }
+
+
+       public boolean rebindAsUser() {
+               if (userBindRequest == null || currentBindRequest == userBindRequest) {
+                       return false;
+               }
+               try {
+                       conn.bind(userBindRequest);
+                       currentBindRequest = userBindRequest;
+               } catch (LDAPException e) {
+                       conn.close();
+                       logger.error("Error rebinding to LDAP with user account.", e);
+                       return false;
+               }
+               return true;
+       }
+
+
+
+       public SearchResult search(SearchRequest request) {
+               try {
+                       return conn.search(request);
+               } catch (LDAPSearchException e) {
+                       logger.error("Problem Searching LDAP [{}]",  e.getResultCode());
+                       return e.getSearchResult();
+               }
+       }
+
+
+       public SearchResult search(String base, boolean dereferenceAliases, String filter, List<String> attributes) {
+               try {
+                       SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
+                       if (dereferenceAliases) {
+                               searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
+                       }
+                       if (attributes != null) {
+                               searchRequest.setAttributes(attributes);
+                       }
+                       SearchResult result = search(searchRequest);
+                       return result;
+
+               } catch (LDAPException e) {
+                       logger.error("Problem creating LDAP search", e);
+                       return null;
+               }
+       }
+
+
+
+       public boolean isAuthenticated(String userDn, String password) {
+               verifyCurrentBinding();
+
+               // If the currently bound DN is already the DN of the logging in user, authentication has already happened
+               // during the previous bind operation. We accept this and return with the current bind left in place.
+               // This could also be changed to always retry binding as the logging in user, to make sure that the
+               // connection binding has not been tampered with in between. So far I see no way how this could happen
+               // and thus skip the repeated binding.
+               // This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
+               // when searching the user entry.
+               String boundDN = currentBindRequest.getBindDN();
+               if (boundDN != null && boundDN.equals(userDn)) {
+                       return true;
+               }
+
+               // Bind a the logging in user to check for authentication.
+               // Afterwards, bind as the original bound DN again, to restore the previous authorization.
+               boolean isAuthenticated = false;
+               try {
+                       // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
+                       SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
+                       conn.bind(ubr);
+                       isAuthenticated = true;
+                       userBindRequest = ubr;
+               } catch (LDAPException e) {
+                       logger.error("Error authenticating user ({})", userDn, e);
+               }
+
+               try {
+                       conn.bind(currentBindRequest);
+               } catch (LDAPException e) {
+                       logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
+                                               e.getResultCode(), e);
+               }
+               return isAuthenticated;
+       }
+
+
+
+       private boolean verifyCurrentBinding() {
+               BindRequest lastBind = conn.getLastBindRequest();
+               if (lastBind == currentBindRequest) {
+                       return true;
+               }
+               logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);
+
+               String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
+               String boundDN = currentBindRequest.getBindDN();
+               logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
+               if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
+                       logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
+                       logger.warn("Updated binding information in LDAP connection.");
+                       currentBindRequest = (SimpleBindRequest)lastBind;
+                       return false;
+               }
+               return true;
+       }
+}
diff --git a/src/test/java/com/gitblit/tests/LdapConnectionTest.java b/src/test/java/com/gitblit/tests/LdapConnectionTest.java
new file mode 100644 (file)
index 0000000..f8d2fed
--- /dev/null
@@ -0,0 +1,248 @@
+package com.gitblit.tests;
+
+import static org.junit.Assume.assumeTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import com.gitblit.Keys;
+import com.gitblit.ldap.LdapConnection;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchRequest;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+
+/*
+ * Test for the LdapConnection
+ *
+ * @author Florian Zschocke
+ *
+ */
+@RunWith(Parameterized.class)
+public class LdapConnectionTest extends LdapBasedUnitTest {
+
+       @Test
+       public void testEscapeLDAPFilterString() {
+               // This test is independent from authentication mode, so run only once.
+               assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+               // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
+               assertEquals("No special characters to escape", "Hi This is a test #çà", LdapConnection.escapeLDAPSearchFilter("Hi This is a test #çà"));
+               assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is \\2a a \\5c test # ç à ô", LdapConnection.escapeLDAPSearchFilter("Hi (This) = is * a \\ test # ç à ô"));
+
+               assertEquals("Injection", "\\2a\\29\\28userPassword=secret", LdapConnection.escapeLDAPSearchFilter("*)(userPassword=secret"));
+       }
+
+
+       @Test
+       public void testConnect() {
+               // This test is independent from authentication mode, so run only once.
+               assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+       @Test
+       public void testBindAnonymous() {
+               // This test tests for anonymous bind, so run only in authentication mode ANONYMOUS.
+               assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+
+                       BindResult br = conn.bind();
+                       assertNotNull(br);
+                       assertEquals(ResultCode.SUCCESS, br.getResultCode());
+                       assertEquals("", authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+       @Test
+       public void testBindAsAdmin() {
+               // This test tests for anonymous bind, so run only in authentication mode DS_MANAGER.
+               assumeTrue(authMode == AuthMode.DS_MANAGER);
+
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+
+                       BindResult br = conn.bind();
+                       assertNotNull(br);
+                       assertEquals(ResultCode.SUCCESS, br.getResultCode());
+                       assertEquals(settings.getString(Keys.realm.ldap.username, "UNSET"), authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+       @Test
+       public void testBindToBindpattern() {
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+
+                       String bindPattern = "CN=${username},OU=Canada," + ACCOUNT_BASE;
+
+                       BindResult br = conn.bind(bindPattern, "UserThree", "userThreePassword");
+                       assertNotNull(br);
+                       assertEquals(ResultCode.SUCCESS, br.getResultCode());
+                       assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+                       br = conn.bind(bindPattern, "UserFour", "userThreePassword");
+                       assertNull(br);
+
+                       br = conn.bind(bindPattern, "UserTwo", "userTwoPassword");
+                       assertNull(br);
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+       @Test
+       public void testRebindAsUser() {
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+
+                       assertFalse(conn.rebindAsUser());
+
+                       BindResult br = conn.bind();
+                       assertNotNull(br);
+                       assertFalse(conn.rebindAsUser());
+
+
+                       String bindPattern = "CN=${username},OU=Canada," + ACCOUNT_BASE;
+                       br = conn.bind(bindPattern, "UserThree", "userThreePassword");
+                       assertNotNull(br);
+                       assertFalse(conn.rebindAsUser());
+
+                       br = conn.bind();
+                       assertNotNull(br);
+                       assertTrue(conn.rebindAsUser());
+                       assertEquals(ResultCode.SUCCESS, br.getResultCode());
+                       assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, authMode.getBindTracker().getLastSuccessfulBindDN());
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+
+       @Test
+       public void testSearchRequest() throws LDAPException {
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+                       BindResult br = conn.bind();
+                       assertNotNull(br);
+
+                       SearchRequest req;
+                       SearchResult result;
+                       SearchResultEntry entry;
+
+                       req = new SearchRequest(ACCOUNT_BASE, SearchScope.BASE, "(CN=UserOne)");
+                       result = conn.search(req);
+                       assertNotNull(result);
+                       assertEquals(0, result.getEntryCount());
+
+                       req = new SearchRequest(ACCOUNT_BASE, SearchScope.ONE, "(CN=UserTwo)");
+                       result = conn.search(req);
+                       assertNotNull(result);
+                       assertEquals(0, result.getEntryCount());
+
+                       req = new SearchRequest(ACCOUNT_BASE, SearchScope.SUB, "(CN=UserThree)");
+                       result = conn.search(req);
+                       assertNotNull(result);
+                       assertEquals(1, result.getEntryCount());
+                       entry = result.getSearchEntries().get(0);
+                       assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, entry.getDN());
+
+                       req = new SearchRequest(ACCOUNT_BASE, SearchScope.SUBORDINATE_SUBTREE, "(CN=UserFour)");
+                       result = conn.search(req);
+                       assertNotNull(result);
+                       assertEquals(1, result.getEntryCount());
+                       entry = result.getSearchEntries().get(0);
+                       assertEquals("CN=UserFour,OU=Canada," + ACCOUNT_BASE, entry.getDN());
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+
+       @Test
+       public void testSearch() throws LDAPException {
+               LdapConnection conn = new LdapConnection(settings);
+               try {
+                       assertTrue(conn.connect());
+                       BindResult br = conn.bind();
+                       assertNotNull(br);
+
+                       SearchResult result;
+                       SearchResultEntry entry;
+
+                       result = conn.search(ACCOUNT_BASE, false, "(CN=UserOne)", null);
+                       assertNotNull(result);
+                       assertEquals(1, result.getEntryCount());
+                       entry = result.getSearchEntries().get(0);
+                       assertEquals("CN=UserOne,OU=US," + ACCOUNT_BASE, entry.getDN());
+
+                       result = conn.search(ACCOUNT_BASE, true, "(&(CN=UserOne)(surname=One))", null);
+                       assertNotNull(result);
+                       assertEquals(1, result.getEntryCount());
+                       entry = result.getSearchEntries().get(0);
+                       assertEquals("CN=UserOne,OU=US," + ACCOUNT_BASE, entry.getDN());
+
+                       result = conn.search(ACCOUNT_BASE, true, "(&(CN=UserOne)(surname=Two))", null);
+                       assertNotNull(result);
+                       assertEquals(0, result.getEntryCount());
+
+                       result = conn.search(ACCOUNT_BASE, true, "(surname=Two)", Arrays.asList("givenName", "surname"));
+                       assertNotNull(result);
+                       assertEquals(1, result.getEntryCount());
+                       entry = result.getSearchEntries().get(0);
+                       assertEquals("CN=UserTwo,OU=US," + ACCOUNT_BASE, entry.getDN());
+                       assertEquals(2, entry.getAttributes().size());
+                       assertEquals("User", entry.getAttributeValue("givenName"));
+                       assertEquals("Two", entry.getAttributeValue("surname"));
+
+                       result = conn.search(ACCOUNT_BASE, true, "(personalTitle=Mr*)", null);
+                       assertNotNull(result);
+                       assertEquals(3, result.getEntryCount());
+                       ArrayList<String> names = new ArrayList<>(3);
+                       names.add(result.getSearchEntries().get(0).getAttributeValue("surname"));
+                       names.add(result.getSearchEntries().get(1).getAttributeValue("surname"));
+                       names.add(result.getSearchEntries().get(2).getAttributeValue("surname"));
+                       assertTrue(names.contains("One"));
+                       assertTrue(names.contains("Two"));
+                       assertTrue(names.contains("Three"));
+
+               } finally {
+                       conn.close();
+               }
+       }
+
+}