| /* |
| * 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; |
| } |
| } |