/* * Copyright 2016 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ 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 static String getAccountBase(IStoredSettings settings) { return settings.getString(Keys.realm.ldap.accountBase, ""); } public static String getAccountPattern(IStoredSettings settings) { return settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))"); } 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 String getAccountBase() { return getAccountBase(settings); } public String getAccountPattern() { return getAccountPattern(settings); } 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 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; } 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 SearchResult searchUser(String username, List<String> attributes) { String accountPattern = getAccountPattern(); accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(username)); return search(getAccountBase(), false, accountPattern, attributes); } public SearchResult searchUser(String username) { return searchUser(username, null); } 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; } }