diff options
author | Florian Zschocke <florian.zschocke@devolo.de> | 2016-11-23 02:59:39 +0100 |
---|---|---|
committer | Florian Zschocke <florian.zschocke@devolo.de> | 2016-11-23 02:59:39 +0100 |
commit | 967c2422591b70a82bd8fc991e87088e880f5024 (patch) | |
tree | 9a86e2fee4e9d97359b18e9cde7d8e73b92ba22c /src/main/java/com | |
parent | 8d27912b0f7f0a67a929671a9c6ff3c8052e3497 (diff) | |
download | gitblit-967c2422591b70a82bd8fc991e87088e880f5024.tar.gz gitblit-967c2422591b70a82bd8fc991e87088e880f5024.zip |
Extract LdapConnection into new class from LdapAuthProvider
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.
Diffstat (limited to 'src/main/java/com')
-rw-r--r-- | src/main/java/com/gitblit/auth/LdapAuthProvider.java | 275 | ||||
-rw-r--r-- | src/main/java/com/gitblit/ldap/LdapConnection.java | 288 |
2 files changed, 295 insertions, 268 deletions
diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java index 19fd4632..8a326cdc 100644 --- a/src/main/java/com/gitblit/auth/LdapAuthProvider.java +++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java @@ -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 index 00000000..b7f07a1e --- /dev/null +++ b/src/main/java/com/gitblit/ldap/LdapConnection.java @@ -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; + } +} |