diff options
author | James Moger <james.moger@gmail.com> | 2016-11-14 17:42:10 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-14 17:42:10 -0500 |
commit | 6832f1937942c5f499327c0c3fcb6307a6b7cf85 (patch) | |
tree | c9e12b13de28ffa7ab9a6913d071c875cdaa793e /src/main | |
parent | 35f149120cc1ab874f7fa8c1a299006a90759da9 (diff) | |
parent | f004a7f1d6bd9eaa6e7a8c8cd9ddae4187bd9994 (diff) | |
download | gitblit-6832f1937942c5f499327c0c3fcb6307a6b7cf85.tar.gz gitblit-6832f1937942c5f499327c0c3fcb6307a6b7cf85.zip |
Merge pull request #1149 from fzs/fixLDAPbinding
Fix LDAP binding strategies
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/distrib/data/defaults.properties | 17 | ||||
-rw-r--r-- | src/main/java/com/gitblit/auth/LdapAuthProvider.java | 398 |
2 files changed, 299 insertions, 116 deletions
diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index 208fd992..04166342 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties @@ -1812,6 +1812,10 @@ realm.salesforce.orgId = 0 realm.ldap.server = ldap://localhost # Login username for LDAP searches. +# This is usually a user with permissions to search LDAP users and groups. +# It must have at least have the permission to search users. If it does not +# have permission to search groups, the normal user logging in must have +# the permission in LDAP to search groups. # If this value is unspecified, anonymous LDAP login will be used. # # e.g. mydomain\\username @@ -1824,8 +1828,14 @@ realm.ldap.username = cn=Directory Manager # SINCE 1.0.0 realm.ldap.password = password -# Bind pattern for Authentication. -# Allow to directly authenticate an user without LDAP Searches. +# Bind pattern for user authentication. +# Allow to directly authenticate an user without searching for it in LDAP. +# Use this if the LDAP server does not allow anonymous access and you don't +# want to use a specific account to run searches. When set, it will override +# the settings realm.ldap.username and realm.ldap.password. +# This requires that all relevant user entries are children to the same DN, +# and that logging users have permission to search for their groups in LDAP. +# This will disable synchronization as a specific LDAP account is needed for that. # # e.g. CN=${username},OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain # @@ -1941,6 +1951,9 @@ realm.ldap.email = email realm.ldap.uid = uid # Defines whether to synchronize all LDAP users and teams into the user service +# This requires either anonymous LDAP access or that a specific account is set +# in realm.ldap.username and realm.ldap.password, that has permission to read +# users and groups in LDAP. # # Valid values: true, false # If left blank, false is assumed diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java index cc772e7b..e1dec48f 100644 --- a/src/main/java/com/gitblit/auth/LdapAuthProvider.java +++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java @@ -39,6 +39,8 @@ 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; @@ -107,8 +109,14 @@ 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 = getLdapConnection(); - if (ldapConnection != null) { + LdapConnection ldapConnection = new LdapConnection(); + if (ldapConnection.connect()) { + if (ldapConnection.bind() == null) { + ldapConnection.close(); + logger.error("Cannot synchronize with LDAP."); + return; + } + try { String accountBase = settings.getString(Keys.realm.ldap.accountBase, ""); String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid"); @@ -179,66 +187,6 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { } } - private LDAPConnection getLdapConnection() { - try { - - URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); - String ldapHost = ldapUrl.getHost(); - int ldapPort = ldapUrl.getPort(); - String bindUserName = settings.getString(Keys.realm.ldap.username, ""); - String bindPassword = settings.getString(Keys.realm.ldap.password, ""); - - LDAPConnection conn; - 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 null; - } - - 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()); - } - } - - if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) { - // anonymous bind - conn.bind(new SimpleBindRequest()); - } else { - // authenticated bind - conn.bind(new SimpleBindRequest(bindUserName, bindPassword)); - } - - return conn; - - } 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 null; - } - /** * Credentials are defined in the LDAP server and can not be manipulated * from Gitblit. @@ -321,23 +269,26 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { public UserModel authenticate(String username, char[] password) { String simpleUsername = getSimpleUsername(username); - LDAPConnection ldapConnection = getLdapConnection(); - if (ldapConnection != null) { - try { - boolean alreadyAuthenticated = false; + LdapConnection ldapConnection = new LdapConnection(); + if (ldapConnection.connect()) { - String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, ""); - if (!StringUtils.isEmpty(bindPattern)) { - try { - String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername)); - ldapConnection.bind(bindUser, new String(password)); + // Try to bind either to the "manager" account, + // or directly to the DN of the user logging in, if realm.ldap.bindpattern is configured. + String passwd = new String(password); + BindResult bindResult = null; + String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, ""); + if (! StringUtils.isEmpty(bindPattern)) { + bindResult = ldapConnection.bind(bindPattern, simpleUsername, passwd); + } else { + bindResult = ldapConnection.bind(); + } + if (bindResult == null) { + ldapConnection.close(); + return null; + } - alreadyAuthenticated = true; - } catch (LDAPException e) { - return null; - } - } + try { // 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}))"); @@ -348,7 +299,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { SearchResultEntry loggingInUser = result.getSearchEntries().get(0); String loggingInUserDN = loggingInUser.getDN(); - if (alreadyAuthenticated || isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) { + if (ldapConnection.isAuthenticated(loggingInUserDN, passwd)) { logger.debug("LDAP authenticated: " + username); UserModel user = null; @@ -462,7 +413,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { } } - private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) { + private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) { String loggingInUserDN = loggingInUser.getDN(); // Clear the users team memberships - we're going to get them from LDAP @@ -479,7 +430,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue())); } - SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn")); + SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn")); if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) { for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) { SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); @@ -496,12 +447,12 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { } } - private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) { + private void getEmptyTeamsFromLdap(LdapConnection ldapConnection) { logger.info("Start fetching empty teams from ldap."); String groupBase = settings.getString(Keys.realm.ldap.groupBase, ""); String groupMemberPattern = settings.getString(Keys.realm.ldap.groupEmptyMemberPattern, "(&(objectClass=group)(!(member=*)))"); - SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, null); + SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, null); if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) { for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) { SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i); @@ -519,6 +470,30 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { logger.info("Finished fetching empty teams from ldap."); } + + private SearchResult searchTeamsInLdap(LdapConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) { + SearchResult result = ldapConnection.search(base, dereferenceAliases, filter, attributes); + if (result == null) { + return null; + } + + if (result.getResultCode() != ResultCode.SUCCESS) { + // Retry the search with user authorization in case we searched as a manager account that could not search for teams. + logger.debug("Rebinding as user to search for teams in LDAP"); + result = null; + if (ldapConnection.rebindAsUser()) { + result = ldapConnection.search(base, dereferenceAliases, filter, attributes); + if (result.getResultCode() != ResultCode.SUCCESS) { + return null; + } + logger.info("Successful search after rebinding as user."); + } + } + + return result; + } + + private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) { TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn")); answer.accountType = getAccountType(); @@ -527,47 +502,21 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { return answer; } - private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) { - try { - return ldapConnection.search(base, SearchScope.SUB, filter); - } catch (LDAPSearchException e) { - logger.error("Problem Searching LDAP", e); - - return null; - } - } - - private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) { + private SearchResult doSearch(LdapConnection ldapConnection, String base, String filter) { try { SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter); - if (dereferenceAliases) { - searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING); - } - if (attributes != null) { - searchRequest.setAttributes(attributes); + SearchResult result = ldapConnection.search(searchRequest); + if (result.getResultCode() != ResultCode.SUCCESS) { + return null; } - return ldapConnection.search(searchRequest); - - } catch (LDAPSearchException e) { - logger.error("Problem Searching LDAP", e); - - return null; + return result; } catch (LDAPException e) { logger.error("Problem creating LDAP search", e); return null; } } - private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) { - try { - // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN - ldapConnection.bind(userDn, password); - return true; - } catch (LDAPException e) { - logger.error("Error authenticating user", e); - return false; - } - } + /** * Returns a simple username without any domain prefixes. @@ -585,7 +534,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { } // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java - public static final String escapeLDAPSearchFilter(String filter) { + private static final String escapeLDAPSearchFilter(String filter) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < filter.length(); i++) { char curChar = filter.charAt(i); @@ -625,4 +574,225 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider { } } + + + 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; + } + } } |