// 2012-10-08 johnpfeiffer requires validation-api, bval-core, bval-jsr303, commons-beanutils-core, commons-lang3
// TODO: check for number of arguments passed?
// TODO: ensure LDAPS with self signed
// TODO: self root CA + intermediate + ssl
package net.kittyandbear.util;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class LdapSearch
{
public static final String CLASSVERSION = "0.35";
private static final String CONNECTIONTIMEOUTMILLISECONDS = "15000";
private static final String READQUERYTIMEOUTMILLISECONDS = "15000";
private long runtime;
// @NotNull( message = "ERROR: Directory Context cannot be null" )
DirContext ctx = null;
@NotNull( message = "ERROR: hostname cannot be null" )
private String hostname;
@Min( value = 0 , message = "ERROR: port number too small, must be between 0 and 65535" )
@Max( value = 65535 , message = "ERROR: port number too large, must be between 0 and 65535" )
private int port;
@NotNull( message = "ERROR: baseDN cannot be null" )
private LdapName baseDN;
@NotNull( message = "ERROR: binding user DN cannot be null" )
private LdapName bindUserDN;
@NotNull( message = "ERROR: binding user password cannot be null" )
private String bindUserPassword;
@NotNull( message = "ERROR: user attribute cannot be null" )
private String userAttribute;// = "sAMAccountName"; // ldap uses uid
@NotNull( message = "ERROR: search base cannot be null" )
private LdapName searchBase;
public static class Builder
{
DirContext ctx = null;
private String hostname;
private int port;
private LdapName baseDN;
private LdapName bindUserDN;
private String bindUserPassword;
private String userAttribute;
private LdapName searchBase;
public Builder hostname( String value )
{
this.hostname = value;
return this;
}
public Builder port( int value )
{
this.port = value;
return this;
}
public Builder bindUserDN( LdapName value )
{
this.bindUserDN = value;
return this;
}
public Builder password( String value )
{
this.bindUserPassword = value;
return this;
}
public Builder baseDN( LdapName value )
{
this.baseDN = value;
return this;
}
public Builder userAttribute( String value )
{
this.userAttribute = value;
return this;
}
public Builder searchBase( LdapName value )
{
this.searchBase = value;
return this;
}
public LdapSearch build() throws IllegalArgumentException
{
LdapSearch ldap = new LdapSearch( this ); // object may exist in an incomplete or illegal state
LdapSearchValidator( ldap );
return ldap;
}
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private void LdapSearchValidator( LdapSearch ldap ) throws IllegalArgumentException
{
Set <ConstraintViolation <LdapSearch>> violations = validator.validate( ldap );
if( !violations.isEmpty() )
{
for( ConstraintViolation <?> v : violations )
{
throw new IllegalArgumentException( v.getMessage() );
}
}
// ConstraintViolations ensure all of the values are non null
if( this.hostname.isEmpty() )
{
throw new IllegalArgumentException( "ERROR: hostname cannot be empty" );
}
try
{
InetAddress.getByName( this.hostname );
}catch( Exception e )
{
throw new IllegalArgumentException( "ERROR: hostname " + e.getMessage() );
}
if( this.userAttribute.isEmpty() )
{
throw new IllegalArgumentException( "ERROR: userAttribute cannot be empty" );
}
if( !this.userAttribute.equalsIgnoreCase( "uid" ) && !this.userAttribute.toLowerCase().equalsIgnoreCase( "samaccountname" ) )
{
throw new IllegalArgumentException( "ERROR: userAttribute is unsupported: " + this.userAttribute );
}
}
} // end inner class Builder
private LdapSearch( Builder builder ) throws IllegalArgumentException
{
this.hostname = builder.hostname;
this.port = builder.port;
this.baseDN = builder.baseDN;
this.bindUserDN = builder.bindUserDN;
this.bindUserPassword = builder.bindUserPassword;
this.baseDN = builder.baseDN;
this.searchBase = builder.searchBase;
this.userAttribute = builder.userAttribute;
}
private long calculateRuntime( long start )
{
return ( System.currentTimeMillis() - start );
}
public ArrayList <String> queryForLdapName( LdapName ldapName ) throws SocketTimeoutException
{
long startTimeMilliseconds = System.currentTimeMillis();
Hashtable <String , String> env = buildEnvironment();
ArrayList <String> results = new ArrayList <String>();
try
{
ctx = new InitialDirContext( env );
results = searchWithFilterLdapName( ctx , ldapName );
}catch( NamingException ne )
{
if( ne.getRootCause() != null )
{
results.add( ne.getRootCause() + " " );
}
results.add( ne.getMessage() );
this.runtime = calculateRuntime( startTimeMilliseconds );
long connectionTimeoutMax = Long.parseLong( CONNECTIONTIMEOUTMILLISECONDS );
if( connectionTimeoutMax < runtime )
{
results.add( " " + runtime + " exceeds timeout max " + connectionTimeoutMax );
}
}
results.add( "Query took " + calculateRuntime( startTimeMilliseconds ) + " ms" );
if( ctx != null )
{
try
{
ctx.close();
}catch( Exception e )
{
}
}
return results;
}
public ArrayList <String> queryForBindingUser()
{
long startTimeMilliseconds = System.currentTimeMillis();
long endTimeMilliseconds;
Hashtable <String , String> env = buildEnvironment();
ArrayList <String> results = new ArrayList <String>();
try
{
ctx = new InitialDirContext( env );
String userName = getUserNameFromBindingUserDN();
LdapName ldapName = new LdapName( userName );
results = searchWithFilterLdapName( ctx , ldapName );
}catch( SocketTimeoutException ste )
{
results.add( "ERROR: SocketTimeoutException: " + ste.getMessage() );
}catch( NamingException ne )
{
if( ne.getRootCause() != null )
{
results.add( ne.getRootCause() + " " );
}
results.add( ne.getMessage() );
endTimeMilliseconds = System.currentTimeMillis();
runtime = endTimeMilliseconds - startTimeMilliseconds;
long connectionTimeoutMax = Long.parseLong( CONNECTIONTIMEOUTMILLISECONDS );
if( connectionTimeoutMax < runtime )
{
results.add( " " + runtime + " exceeds timeout max " + connectionTimeoutMax );
}
}
results.add( "Query took " + calculateRuntime( startTimeMilliseconds ) + " ms" );
if( ctx != null )
{
try
{
ctx.close();
}catch( Exception e )
{
}
}
return results;
}
private Hashtable <String , String> buildEnvironment()
{
Hashtable <String , String> env = new Hashtable <String , String>();
env.put( Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( "com.sun.jndi.ldap.connect.pool" , "true" );
env.put( "com.sun.jndi.ldap.connect.timeout" , CONNECTIONTIMEOUTMILLISECONDS );
env.put( "com.sun.jndi.ldap.read.timeout" , READQUERYTIMEOUTMILLISECONDS ); // time that a query can take
env.put( Context.REFERRAL , "follow" );
env.put( Context.SECURITY_AUTHENTICATION , "simple" );
env.put( Context.SECURITY_PRINCIPAL , bindUserDN.toString() );
env.put( Context.SECURITY_CREDENTIALS , bindUserPassword );
env.put( Context.PROVIDER_URL , "ldap://" + hostname + ":" + port + "/" + baseDN );
return env;
}
private String getUserNameFromBindingUserDN()
{
String userName = "";
for( Enumeration <String> names = this.bindUserDN.getAll() ; names.hasMoreElements() ; )
{
userName = names.nextElement();
}
return userName;
}
private ArrayList <String> searchWithFilterLdapName( DirContext ctx , LdapName ldapName ) throws NamingException , SocketTimeoutException
{
ArrayList <String> resultList = new ArrayList <String>();
String[] attributeFilter =
{ "distinguishedname", this.userAttribute };
SearchControls searchcontrols = buildSearchControls( attributeFilter );
String searchFilter = "(" + ldapName + ")";
NamingEnumeration <SearchResult> results = null;
try
{
results = ctx.search( this.searchBase , searchFilter , searchcontrols );
while( results.hasMore() )
{
SearchResult sr = results.next();
Attributes attrs = sr.getAttributes();
resultList.add( attrs.toString() );
// resultList.add( sr.getNameInNamespace() );
// System.out.println( attrs.getIDs() );
// Attribute attr = attrs.getIDs()
// resultList.add( )
// Attribute attr = attrs.get( userAttribute );
// System.out.println( "RETRIEVED: " + attr.get() );
}
}catch( PartialResultException e )
{ // System.out.println( "ignoring partial result exception" );
}
if( results != null )
{
try
{
results.close();
}catch( Exception e )
{
}
}
return resultList;
}
private SearchControls buildSearchControls( String[] attributeFilter )
{
SearchControls searchcontrols = new SearchControls(); // tree limit, count limit, time limit, attribs to return
searchcontrols.setSearchScope( SearchControls.SUBTREE_SCOPE );
searchcontrols.setReturningAttributes( attributeFilter );
return searchcontrols;
}
} // end class