All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.ldap.LDAPClient Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
package com.hfg.ldap;


import java.lang.reflect.Method;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.net.ssl.SSLHandshakeException;

import com.hfg.cert.CertificateUtil;
import com.hfg.exception.InvalidValueException;
import com.hfg.html.Col;
import com.hfg.ldap.ad.ActiveDirectory;
import com.hfg.security.LoginCredentials;
import com.hfg.util.StackTraceUtil;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;

import static com.hfg.cert.CertificateUtil.geLdapCertificates;

//------------------------------------------------------------------------------
/**
 Simple LDAP Client.
 
@author J. Alex Taylor, hairyfatguy.com
*/ //------------------------------------------------------------------------------ // com.hfg XML/HTML Coding Library // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com // [email protected] //------------------------------------------------------------------------------ public class LDAPClient { private static final ReferralHandling sDefaultReferralHandling = ReferralHandling.FOLLOW_REFERRALS; private static final Logger LOGGER = Logger.getLogger(LDAPClient.class.getPackage().getName()); private String mLDAP_ServerURL; private LdapName mLDAP_UserContext; private LdapName mLDAP_GroupContext; private String mLDAP_PrincipalFieldName = "uid"; private LdapName mLDAP_PrincipalContext; private String mLDAP_PrincipalSuffix; private LoginCredentials mLDAP_PrincipalCredentials; private LDAP_UserObjFactory mUserFactory = new LDAP_UserFactory(); private ReferralHandling mReferralHandling = sDefaultReferralHandling; private Set mSupportedControls; private static final String PAGING_OID = "1.2.840.113556.1.4.319"; private static final String VLV_OID = "2.16.840.1.113730.3.4.9"; public enum ReferralHandling { FOLLOW_REFERRALS("follow"), IGNORE_REFERRALS("ignore"), THROW_REFERRALS("throw"); private String mValue; private ReferralHandling(String inValue) { mValue = inValue; } public String getValue() { return mValue; } @Override public String toString() { return getValue(); } } static { LOGGER.setLevel(Level.WARNING); LOGGER.setUseParentHandlers(true); } //########################################################################### // CONSTRUCTORS //########################################################################### //------------------------------------------------------------------------ public LDAPClient() { } //------------------------------------------------------------------------ public LDAPClient(LDAP_Config inConfig) { this(); setServerURL(inConfig.getServerURL()); setUserContext(inConfig.getUserContext()); setPrincipalFieldName(inConfig.getPrincipalFieldName()); setPrincipalCredentials(inConfig.getPrincipalCredentials()); setReferralHandling(inConfig.getReferralHandling()); } //########################################################################### // PUBLIC METHODS //########################################################################### //--------------------------------------------------------------------------- public static Logger getLogger() { return LOGGER; } //--------------------------------------------------------------------------- public LDAPClient setServerURL(String inValue) { mLDAP_ServerURL = inValue; return this; } //--------------------------------------------------------------------------- public String getServerURL() { return mLDAP_ServerURL; } //--------------------------------------------------------------------------- public LDAPClient setUserContext(String inValue) { try { setUserContext(inValue != null ? new LdapName(inValue) : null); } catch (InvalidNameException e) { throw new RuntimeException(e); } return this; } //--------------------------------------------------------------------------- public LDAPClient setReferralHandling(ReferralHandling inValue) { mReferralHandling = inValue; return this; } //--------------------------------------------------------------------------- public ReferralHandling getReferralHandling() { return mReferralHandling; } //--------------------------------------------------------------------------- public LDAPClient setUserContext(LdapName inValue) { mLDAP_UserContext = inValue; return this; } //--------------------------------------------------------------------------- public LDAPClient setGroupContext(String inValue) { try { setGroupContext(inValue != null ? new LdapName(inValue) : null); } catch (InvalidNameException e) { throw new RuntimeException(e); } return this; } //--------------------------------------------------------------------------- public LDAPClient setGroupContext(LdapName inValue) { mLDAP_GroupContext = inValue; return this; } //--------------------------------------------------------------------------- public LDAPClient setPrincipalContext(String inValue) { try { setPrincipalContext(inValue != null ? new LdapName(inValue) : null); } catch (InvalidNameException e) { throw new RuntimeException(e); } return this; } //--------------------------------------------------------------------------- public LDAPClient setPrincipalContext(LdapName inValue) { mLDAP_PrincipalContext = inValue; return this; } //--------------------------------------------------------------------------- public LDAPClient setPrincipalSuffix(String inValue) { mLDAP_PrincipalSuffix = inValue; return this; } //--------------------------------------------------------------------------- /** Specifies the credentials to be used to access the LDAP server. * @param inValue the security principal credentials * @return this LDAPClient object to facilitate method chaining */ public LDAPClient setPrincipalCredentials(LoginCredentials inValue) { mLDAP_PrincipalCredentials = inValue; return this; } //--------------------------------------------------------------------------- public LDAPClient setPrincipalFieldName(String inValue) { mLDAP_PrincipalFieldName = inValue; return this; } //--------------------------------------------------------------------------- public LDAP_UserObjFactory getUserFactory() { return mUserFactory; } //--------------------------------------------------------------------------- public LDAPClient setUserFactory(LDAP_UserObjFactory inValue) { if (null == inValue) { throw new InvalidValueException("The user factory cannot be set to null!"); } mUserFactory = inValue; return this; } //--------------------------------------------------------------------------- // TODO: Should this method return a more complex result that could contain more info about failures? public boolean authenticate(LoginCredentials inCredentials) { crosscheck(); boolean authenticated = false; DirContext ctx = null; try { // Create the initial context ctx = getInitialDirContext(inCredentials); if (ctx != null) { authenticated = true; } /* // get more attributes about this user SearchControls scs = new SearchControls(); scs.setSearchScope(SearchControls.SUBTREE_SCOPE); String[] attrNames = { "mail", "cn" }; scs.setReturningAttributes(attrNames); NamingEnumeration nes = ctx.search(mLDAP_UserContext, "uid=" + userName, scs); if(nes.hasMore()) { Attributes attrs = ((SearchResult) nes.next()).getAttributes(); System.out.println("mail: " + attrs.get("mail").get()); System.out.println("cn: " + attrs.get("cn").get()); } */ } catch (AuthenticationException e) { // If it looks like an Active Directory exception, try to decode it... String msg = ActiveDirectory.decodeAuthenticationException(e); if (StringUtil.isSet(msg)) { new LDAP_Exception(msg, e).printStackTrace(); } else { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } finally { if (ctx != null) { try { ctx.close(); } catch (NamingException e) { // Ignore } } } return authenticated; } //--------------------------------------------------------------------------- public U getInfoForUser(String inQuery) { return getInfoForUser(inQuery, null); } //--------------------------------------------------------------------------- public U getInfoForUser(String inQuery, List inFields) { return getInfoForUser(null, inQuery, inFields); } //--------------------------------------------------------------------------- public U getInfoForUser(LdapName inBaseDN, String inQuery) { return getInfoForUser(inBaseDN, inQuery, null); } //--------------------------------------------------------------------------- public U getInfoForUser(LdapName inBaseDN, String inQuery, List inFields) { List users = getInfoForUsers(inBaseDN, inQuery, inFields); return (CollectionUtil.hasValues(users) ? users.get(0) : null); } //--------------------------------------------------------------------------- public List getInfoForUsers(String inQuery) { return getInfoForUsers(inQuery, null); } //--------------------------------------------------------------------------- public List getInfoForUsers(String inFilter, List inFields) { return getInfoForUsers(null, inFilter, inFields); } //--------------------------------------------------------------------------- public List getInfoForUsers(LdapName inBaseDN, String inFilter, List inFields) { List users; LdapContext ctx = null; try { // Create the initial context ctx = getInitialDirContext(mLDAP_PrincipalCredentials); if (isPagingSupported(ctx)) { users = getInfoForUsersWithPaging(ctx, inBaseDN, inFilter, inFields); } else { users = getInfoForUsersWithoutPaging(ctx, inBaseDN, inFilter, inFields); } } catch (Exception e) { throw composeException(e); } finally { if (ctx != null) { try { ctx.close(); } catch (NamingException e) { } } } int numUsersFound = (CollectionUtil.hasValues(users) ? users.size() : 0); LOGGER.fine(numUsersFound + " user" + (numUsersFound != 1 ? "s" : "") + " found by LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(mLDAP_GroupContext)); return users; } //--------------------------------------------------------------------------- public List getInfoForUsersWithPaging(LdapContext inCtx, LdapName inBaseDN, String inFilter, List inFields) throws Exception { List users = null; LdapName baseDN = inBaseDN; if (null == baseDN) { baseDN = mLDAP_UserContext; // Use the default user context } Map fieldMap = null; // get more attributes about this user SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); String[] attributesToRetrieve = new String[] {"*"}; // Default to all attributes if (CollectionUtil.hasValues(inFields)) { fieldMap = new HashMap<>(inFields.size()); List attrNames = new ArrayList<>(inFields.size()); for (LDAP_UserField field : inFields) { attrNames.add(field.name()); fieldMap.put(field.name(), field); } attributesToRetrieve = attrNames.toArray(new String[] {}); } searchControls.setReturningAttributes(attributesToRetrieve); LOGGER.fine("LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(mLDAP_GroupContext)); // Activate paged results int pageSize = 1000; // 1000 entries per page byte[] cookie = null; int total; inCtx.setRequestControls(new Control[]{ new PagedResultsControl(pageSize, Control.CRITICAL) }); do { // Perform the search NamingEnumeration results = inCtx.search(baseDN, inFilter, searchControls); while (results != null && results.hasMore()) { if (null == users) { users = new ArrayList<>(); } U user = getUserFactory().createUserObj(); users.add(user); Attributes attrs = ((SearchResult) results.next()).getAttributes(); NamingEnumeration attrEnum = attrs.getIDs(); while (attrEnum.hasMoreElements()) { String attrID = attrEnum.nextElement(); if (fieldMap != null) { LDAP_UserField field = fieldMap.get(attrID); if (field != null) { Method setter = field.getUserObjectSetter(); if (setter != null) { try { setter.invoke(user, attrs.get(attrID) .get() .toString()); } catch (Exception e) { } } } } user.setField(attrID, attrs.get(attrID) .get() .toString()); } } // Examine the paged results control response Control[] controls = inCtx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { if (controls[i] instanceof PagedResultsResponseControl) { PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i]; total = prrc.getResultSize(); cookie = prrc.getCookie(); } else { // Handle other response controls (if any) } } } // Re-activate paged results inCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize, cookie, Control.CRITICAL)}); } while (cookie != null); return users; } //--------------------------------------------------------------------------- private List getInfoForUsersWithoutPaging(LdapContext inCtx, LdapName inBaseDN, String inFilter, List inFields) { List users = null; LdapName baseDN = inBaseDN; if (null == baseDN) { baseDN = mLDAP_UserContext; // Use the default user context } try { // get more attributes about this user SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); Map fieldMap = null; String[] attributesToRetrieve = new String[] {"*"}; // Default to all attributes if (CollectionUtil.hasValues(inFields)) { fieldMap = new HashMap<>(inFields.size()); List attrNames = new ArrayList<>(inFields.size()); for (LDAP_UserField field : inFields) { attrNames.add(field.name()); fieldMap.put(field.name(), field); } attributesToRetrieve = attrNames.toArray(new String[] {}); } searchControls.setReturningAttributes(attributesToRetrieve); LOGGER.fine("LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(baseDN)); NamingEnumeration enumeration = inCtx.search(baseDN, inFilter, searchControls); while (enumeration.hasMore()) { if (null == users) { users = new ArrayList<>(); } U user = getUserFactory().createUserObj(); users.add(user); Attributes attrs = ((SearchResult) enumeration.next()).getAttributes(); NamingEnumeration attrEnum = attrs.getIDs(); while (attrEnum.hasMoreElements()) { String attrID = attrEnum.nextElement(); if (fieldMap != null) { LDAP_UserField field = fieldMap.get(attrID); if (field != null) { Method setter = field.getUserObjectSetter(); if (setter != null) { try { setter.invoke(user, attrs.get(attrID).get().toString()); } catch (Exception e) { } } } } user.setField(attrID, attrs.get(attrID).get().toString()); } } } catch (Exception e) { throw composeException(e); } finally { if (inCtx != null) { try { inCtx.close(); } catch (NamingException e) { } } } return users; } //--------------------------------------------------------------------------- public List getUsersForGroup(String inFilter) { List users; LdapContext ctx = null; try { // Create the initial context ctx = getInitialDirContext(mLDAP_PrincipalCredentials); if (isPagingSupported(ctx)) { users = getUsersForGroupWithPaging(ctx, inFilter); } else { users = getUsersForGroupWithoutPaging(ctx, inFilter); } } catch (Exception e) { throw composeException(e); } finally { if (ctx != null) { try { ctx.close(); } catch (NamingException e) { } } } int numUsersFound = (CollectionUtil.hasValues(users) ? users.size() : 0); LOGGER.fine(numUsersFound + " user" + (numUsersFound != 1 ? "s" : "") + " found by LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(mLDAP_GroupContext)); return users; } //--------------------------------------------------------------------------- private List getUsersForGroupWithPaging(LdapContext inCtx, String inFilter) throws Exception { List users = null; // get more attributes about this user SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); String[] attributesToRetrieve = new String[] {"*"}; // Default to all attributes searchControls.setReturningAttributes(attributesToRetrieve); LOGGER.fine("LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(mLDAP_GroupContext)); // Activate paged results int pageSize = 20; // 20 entries per page byte[] cookie = null; int total; inCtx.setRequestControls(new Control[]{ new PagedResultsControl(pageSize, Control.CRITICAL) }); do { // Perform the search NamingEnumeration results = inCtx.search(mLDAP_GroupContext, inFilter, searchControls); while (results != null && results.hasMore()) { Attributes attrs = ((SearchResult) results.next()).getAttributes(); NamingEnumeration attrEnum = attrs.getIDs(); while (attrEnum.hasMoreElements()) { String attrID = attrEnum.nextElement(); if (attrID.equalsIgnoreCase("member") || attrID.equalsIgnoreCase("uniquemember")) { Attribute attr = attrs.get(attrID); users = new ArrayList<>(attr.size()); NamingEnumeration memberEnum = (NamingEnumeration) attr.getAll(); while (memberEnum.hasMoreElements()) { LdapName userDN = new LdapName(memberEnum.nextElement()); users.add(getInfoForUser(userDN, userDN.remove(userDN.size() - 1) .toString())); } } } } // Examine the paged results control response Control[] controls = inCtx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { if (controls[i] instanceof PagedResultsResponseControl) { PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i]; total = prrc.getResultSize(); cookie = prrc.getCookie(); } else { // Handle other response controls (if any) } } } // Re-activate paged results inCtx.setRequestControls(new Control[]{ new PagedResultsControl(pageSize, cookie, Control.CRITICAL) }); } while (cookie != null); return users; } //--------------------------------------------------------------------------- private List getUsersForGroupWithoutPaging(LdapContext inCtx, String inFilter) throws Exception { List users = null; // get more attributes about this user SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); String[] attributesToRetrieve = new String[] {"*"}; // Default to all attributes searchControls.setReturningAttributes(attributesToRetrieve); LOGGER.fine("LDAP Query " + StringUtil.singleQuote(inFilter) + " in context " + StringUtil.singleQuote(mLDAP_GroupContext)); // Perform the search NamingEnumeration results = inCtx.search(mLDAP_GroupContext, inFilter, searchControls); while (results != null && results.hasMore()) { Attributes attrs = ((SearchResult) results.next()).getAttributes(); NamingEnumeration attrEnum = attrs.getIDs(); while (attrEnum.hasMoreElements()) { String attrID = attrEnum.nextElement(); if (attrID.equalsIgnoreCase("member") || attrID.equalsIgnoreCase("uniquemember")) { Attribute attr = attrs.get(attrID); users = new ArrayList<>(attr.size()); NamingEnumeration memberEnum = (NamingEnumeration) attr.getAll(); while (memberEnum.hasMoreElements()) { LdapName userDN = new LdapName(memberEnum.nextElement()); users.add(getInfoForUser(userDN, userDN.remove(userDN.size() - 1).toString())); } } } } return users; } //########################################################################### // PRIVATE METHODS //########################################################################### //--------------------------------------------------------------------------- /** Checks to run before an operation. */ private void crosscheck() { if (null == mLDAP_ServerURL) { throw new LDAP_Exception("No LDAP server has been specified!"); } } //--------------------------------------------------------------------------- private LdapContext getInitialDirContext(LoginCredentials inCredentials) throws Exception { crosscheck(); // creating environment for initial context Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); if (mReferralHandling != null) { // Set how referrals should be handled env.put(Context.REFERRAL, mReferralHandling.getValue()); } LOGGER.info("Setting provider URL to " + StringUtil.singleQuote(mLDAP_ServerURL)); env.put(Context.PROVIDER_URL, mLDAP_ServerURL); // env.put(Context.SECURITY_AUTHENTICATION, "simple"); if (null == inCredentials) { throw new LDAP_Exception("No credentials specified!"); } String securityPrincipal = inCredentials.getUser(); // Active Directory may not want more than this if (mLDAP_PrincipalContext != null) { securityPrincipal = mLDAP_PrincipalFieldName + "=" + inCredentials.getUser() + (mLDAP_PrincipalContext != null ? "," + mLDAP_PrincipalContext : ""); } else if (StringUtil.isSet(mLDAP_PrincipalSuffix) && ! securityPrincipal.endsWith(mLDAP_PrincipalSuffix)) { securityPrincipal += mLDAP_PrincipalSuffix; } LOGGER.info("Setting security principal to " + StringUtil.singleQuote(securityPrincipal)); env.put(Context.SECURITY_PRINCIPAL, securityPrincipal); env.put(Context.SECURITY_CREDENTIALS, inCredentials.getPasswordString()); LOGGER.info("Establishing LDAP connection..."); Control[] controls = null; // Create the initial context return new InitialLdapContext(env, controls); } //--------------------------------------------------------------------------- private boolean isPagingSupported(DirContext inCtx) throws NamingException { return getSupportedControls(inCtx).contains(PAGING_OID); } //--------------------------------------------------------------------------- private Set getSupportedControls(DirContext inCtx) throws NamingException { if (null == mSupportedControls) { Hashtable environment = inCtx.getEnvironment(); URI uri = URI.create("" + environment.get("java.naming.provider.url")); final String rootDse = uri.getScheme() + "://" + uri.getAuthority(); mSupportedControls = new HashSet<>(10); final Attributes attributes = inCtx.getAttributes(rootDse, new String[]{"supportedControl"}); final NamingEnumeration attributeValues = attributes.getAll(); while (attributeValues.hasMore()) { final Attribute attribute = (Attribute) attributeValues.next(); final NamingEnumeration supportedControls = attribute.getAll(); while (supportedControls.hasMore()) { mSupportedControls.add((String) supportedControls.next()); } } } return mSupportedControls; } private LDAP_Exception composeException(Exception inException) { StringBuilderPlus msg = new StringBuilderPlus("Problem querying " + mLDAP_ServerURL + " !"); // If the problem was with a certificate, try to get a summary of the cert. Throwable rootException = StackTraceUtil.getRootException(inException); if (rootException != null && rootException instanceof SSLHandshakeException) { try { Set certs = CertificateUtil.geLdapCertificates(mLDAP_ServerURL); if (CollectionUtil.hasValues(certs)) { msg.appendln(); msg.appendln(CertificateUtil.generateCertSummary(certs.iterator().next())); } } catch (Exception e2) { // Ignore } } return new LDAP_Exception(msg.toString(), inException); } }