Tuesday, September 27, 2016

How to connect LDAP server using Spring framework ? or How to perform search operation in ldap using spring ?



In the following post we will look in to

  • How to connect LDAP server using Spring framework 
  • Simplify directory access with Spring LDAP
  • Using Spring LdapTemplate.
  • How to perform search operation in ldap using spring ?
  • Introduction to Spring LDAP ?
  • How to use Spring with LDAP ?


First I will show a simple example of how to connect to LDAP using plain java that way you will appreciate Spring LDAP template even better.
There are several threads online on this subject. I adding this article so that i can provide more details on how to use 
spring provide LDAP filters. 

Ok lets start with simple Java approach first.
A simple interface that returns users we are looking for as list of Strings or as list of java objects.
Here is the code to do that lets call this class TraditionalLDAPUserRepoImpl.java and the interface as LDAPUserRepo .java

Interface LDAPUserRepo.java
package com.rama.util.activedirectory;
import java.util.List;

public interface  LDAPUserRepo {
 
 /*  Just return usernames that matches your LDAP query criteria. Just as list of Strings*/
 public List<String> getAllPersonNames();
 
 /* This method will return list of java objects  with user name, email etc */
 public List<LDAPUser> getAllPersonNameObjs();
}
Code that show implementation of TraditionalLDAPUserRepoImpl.java
Needless to say you need to change you ldap address and credentials.
// A simple lookup using plain Java 
package com.rama.util.activedirectory;

import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import javax.naming.Context;
import javax.naming.NameNotFoundException;
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.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

public class TraditionalLDAPUserRepoImpl implements LDAPUserRepo {

 public List<String> getAllPersonNames() {
  Hashtable<String, String> authEnv = new Hashtable<String, String>();
  authEnv.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
  //https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap-gl.html
  authEnv.put(Context.PROVIDER_URL, "ldap://dev.rama.int:389/DC=DevS1,DC=int");
     authEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
  //Your  security principla could be different
     authEnv.put(Context.SECURITY_PRINCIPAL,"CN=rama,OU=Service_Account,DC=DevS1,DC=int");
     authEnv.put(Context.SECURITY_CREDENTIALS,"test123");
     
     //https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap-gl.html
     //authEnv.put("java.naming.ldap.referral.limit","10");
     //https://docs.oracle.com/javase/jndi/tutorial/ldap/referral/jndi.html
     //authEnv.put(Context.REFERRAL,"follow");
     //For example, the following code specifies that the provider should block until 24 entries have been read from the server or until the enumeration terminates, whichever produces the fewer number of results:
     //authEnv.put(Context.BATCHSIZE,"400");
      
  DirContext ctx;
  try {
   ctx = new InitialDirContext(authEnv);
  } catch (NamingException e) {
   throw new RuntimeException(e);
  }

  List<String> list = new LinkedList<String>();
  NamingEnumeration results = null;
  try {
   SearchControls controls = new SearchControls();
   controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
   
   /*
    If you comment out the below line all properties will  be pulled.
    Here just to show I am explicitly mentioning the properties we want.
    sn- Simple name
    CN- complete name
    OU - organizational unit.
    
    Think of this as a oracle select statement with the columns you wanted listed in select clause 
   */
   controls.setReturningAttributes(new String[] {"sAMAccountName","givenName", "sn","memberOf" ,"CN","OU","name"});
   
   
   /* 
    This is the condition or filter that will be applied to pull results.
    This is more like where clause in your SQL statement.
    BEWARE: If you give more generic  search criteria then you will have too many records and may get errors.
    Its good practice to restrict or narrow down results to smaller subsets.
   */
   
   results = ctx.search("", "(objectclass=person)", controls);
   
   System.out.println(results);
   while (results.hasMore()) {
    SearchResult searchResult = (SearchResult) results.next();
    Attributes attributes = searchResult.getAttributes();
    //Ok lets pick few that we are interested.
    Attribute memeberOfAttr = attributes.get("memberOf");
    Attribute cnAttr = attributes.get("CN");
    Attribute nameAttr = attributes.get("name");
    Attribute samaAccountNameAttr = attributes.get("sAMAccountName");
    list.add(cnAttr.toString());
    
    
    //Debug only ...
    dumpAll(attributes);
    
    //memberOf in my case can be 1 to many .... So just show hot to pull if we have a collection of 
    //same attribute in LDAP.     
    if(memeberOfAttr!=null){
     String memeberOfStr = "";
     try{
      memeberOfStr = getAllValues(memeberOfAttr);
     }catch(Exception exp){
      exp.printStackTrace();
     }
     System.out.println("samaAccountName="+ samaAccountNameAttr+"| cnAttr="+ cnAttr  +" | name="+nameAttr +" | memeberof="+ memeberOfStr);
    }
    
   }
  } catch (NameNotFoundException e ) {
   // The base context was not found.
   // Just clean up and exit.
  } catch (NamingException e) {
   throw new RuntimeException(e);
  } finally {
   if (results != null) {
    try {
     results.close();
    } catch (Exception e) {
     // Never mind this.
    }
   }
   if (ctx != null) {
    try {
     ctx.close();
    } catch (Exception e) {
     // Never mind this.
    }
   }
  }
  return list;
 }


 private void dumpAll(Attributes attributes) throws NamingException {
  NamingEnumeration<? extends Attribute>  allAttr= attributes.getAll();
  while (allAttr.hasMoreElements()) {
   Attribute attribute = (Attribute) allAttr.nextElement();
   if((attribute.getAll()!= null) &&(attribute.getAll().hasMoreElements())){
    //Ok this Attributes can have one or more values.
    NamingEnumeration<?>  childAttr= attribute.getAll();
    while(childAttr.hasMoreElements()){
     System.out.println("Property="+ attribute.getID() +" Value="+ childAttr.next());
    }
   }else{
    System.out.println("Not All="+ attribute.get().toString());
   }
  }
 }
 
 public List<LDAPUser> getAllPersonNameObjs(){
  // I will show the demo later.
  return null;
 }
 
 
 /*
   Here memeberOf can have many rows.
   memberOf: CN=SCHOOL_MAINT_FULL,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int
   memberOf: CN=REPORT_FULL,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int
   memberOf: CN=LOAN_APP_FULL,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int
   memberOf: CN=DISB_MAINT_READ,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int
  */
 
 public static String getAllValues(Attribute attribute)throws Exception {
  StringBuffer sbuf = new StringBuffer();
  NamingEnumeration<?>  childAttr= attribute.getAll();
  while(childAttr.hasMoreElements()){
   Object obj = childAttr.next();
   sbuf.append("id="+ attribute.getID()+" Value="+ obj.toString()+"\n");
  }
  
  return sbuf.toString();
 }
 
 public static void main(String[] args) {
  LDAPUserRepo personRepo = new TraditionalLDAPUserRepoImpl();
  personRepo.getAllPersonNames();
 }
}

 


Exceptions:.
===========
Some times you will see exceptions like below which indicates that your search query is returning more results and LDAP server may have size restrictions.
The best thing to do is to change you were clause to limit the result set. I have notice that Spring LDAP template was able to fetch more records than plain java solution.

Exception in thread "main" java.lang.RuntimeException: javax.naming.SizeLimitExceededException: [LDAP: error code 4 - Sizelimit Exceeded]; remaining name ''at com.rama.util.activedirectory.TraditionalLDAPUserRepoImpl.getAllPersonNames(TraditionalLDAPUserRepoImpl.java:87)
at com.rama.util.activedirectory.TraditionalLDAPUserRepoImpl.main(TraditionalLDAPUserRepoImpl.java:151)
Caused by: javax.naming.SizeLimitExceededException: [LDAP: error code 4 - Sizelimit Exceeded]; remaining name ''

NOTE:
=====
For troubleshooting and also to just to view I use LDAP browser from Softterra (Google it). Its FREE. (Don't use "LDAP administrator" from softteraa thats a paid one)
Here is snapshot for my user id when I look it up through LDAP browser.
(Here is the link at the time I was writing this blog http://www.ldapadministrator.com/softerra-ldap-browser.htm)











Now the same thing can be done using Spring LdapTemplate.
---------------------------------------------------------
The beauty of Spring ldap template is you can filter data using AndFilter , OrFilter , LikeFilter.
Please see spring package org.springframework.ldap.filter.* for all filters.
The below code will show you how to use filters.

Ok lets create the maven java project.Here is the maven pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.rama.activedirectory</groupId>
 <artifactId>FMActiveDirectory</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>
 <name>FMActiveDirectory</name>
 <url>http://maven.apache.org</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.1</version>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework.ldap</groupId>
   <artifactId>spring-ldap-core</artifactId>
   <version>2.0.4.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-commons-core</artifactId>
   <version>1.4.1.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
   <version>2.6</version>
  </dependency>
 </dependencies>
</project>

Now we need to return as java object so lets create a POJO to get the values.

I will add few properties feel free to enhance it.


package com.rama.util.activedirectory;
import java.util.List;

public class LDAPUser {
 private String sAMAccountName ;
 private String name;
 private String givenName;
 private String canonicalName;
 private List<String> memberOfValueList  = new ArrayList<String>();
 
 // Removed getter and setters for clarity, Please generate it.
 
}

Spring configuration.
===================
Here is how the spring xml looks. I called it spring-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:ldap="http://www.springframework.org/schema/ldap"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/ldap http://www.springframework.org/schema/ldap/spring-ldap.xsd">

   <ldap:context-source
       id="ldapcontextSource" 
          url="ldap://dev.rama.int:389/DC=DevS1,DC=int"  base="DC=DevS1,DC=int"
          username="CN=rama,OU=Service_Account,DC=DevS1,DC=int"  password="test123" />

 <ldap:ldap-template id="ldapTemplate"  context-source-ref="ldapcontextSource"/>

   <bean id="ldapUserRepoBean" class="com.rama.util.activedirectory.SpringTemplateLDAPUSerRepoImpl">
      <property name="ldapTemplate" ref="ldapTemplate" />
   </bean>
</beans> 
Make sure this spring xml file is in your classpath.
Now we need to implement our LDAPUserRepo interface.In order to convert the result set back to java object we need to also implement AttributesMapper interface. Please see the code below.

The code several filters that you can use like AndFilter , OrFilter, LikeFilter etc,
See package "org.springframework.ldap.filter.*" for more filter Options.

SpringTemplateLDAPUSerRepoImpl.java
--------------------------------

package com.rama.util.activedirectory;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

import java.util.ArrayList;
import java.util.List;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.OrFilter;
import org.springframework.ldap.query.LdapQuery;

public class SpringTemplateLDAPUSerRepoImpl implements LDAPUserRepo  {

  private LdapTemplate ldapTemplate;

  public void setLdapTemplate(LdapTemplate ldapTemplate) {
      this.ldapTemplate = ldapTemplate;
   }
    
 /**
  * No Attribute mapper. Simple pull of each property.
  * 
  * @return
  */
 public List<LDAPUser> getAllPersonNameObjs(){
  
  
  //NOTE: Spring doesn't throw javax.naming.SizeLimitExceededException:. Though it cant fetch 
  // all records its remains siletnt.
  // OOPS this is too broad. It may get all memebers from LDAP. But there is a limitation of 1000 in 
  // my case and Spring ldap template pulls thousand and keeps Quite. No exceptions 
  
  LdapQuery query = query().where("objectclass").is("person");
  ldapTemplate.setIgnorePartialResultException(true);
  return ldapTemplate.search(query,new LDAPUSerAttributesMapper());
  
  
  //TO Return reduced result set I tried this to search based on one of the groups the user belongs to. 
  //LdapQuery query = query().where("memberOf").is("CN=DEV_ADMIN_FULL,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int");
  //ldapTemplate.setIgnorePartialResultException(true);
  //return ldapTemplate.search(query,new LDAPUSerAttributesMapper());
  
  
  
  //AndFilter andFilter = new AndFilter();
      //andFilter.and(new EqualsFilter("objectclass","person"));
      //andFilter.and(new EqualsFilter("cn",firstName));
      //andFilter.and(new EqualsFilter("sn",lastName));
      //andFilter.and(new EqualsFilter("ou","User_Accounts"));
   //System.out.println("LDAP Query " + filter.encode());
      //ldapTemplate.setIgnoreSizeLimitExceededException(true);
      //ldapTemplate.setIgnorePartialResultException(true);
         //return ldapTemplate.search("",filter.encode(),new LDAPUSerAttributesMapper());
      
   
  //OrFilter filter = new OrFilter();
    //filter.or(new EqualsFilter("objectclass","person"));
    //filter.or(new EqualsFilter("name","Consultants_Contractors_Temps"));
    //distinguishedName: CN=Ramachandra Reddy,OU=Consultants_Contractors_Temps,OU=User_Accounts,DC=DevS1,DC=int
    //filter.or(new EqualsFilter("distinguishedName","CN=Ramachandra Reddy,OU=Consultants_Contractors_Temps,OU=User_Accounts,DC=DevS1,DC=int"));
   //System.out.println("LDAP Query " + filter.encode());
      //ldapTemplate.setIgnoreSizeLimitExceededException(true);
      //ldapTemplate.setIgnorePartialResultException(true);
         //return ldapTemplate.search("",filter.encode(),new LDAPUSerAttributesMapper());
      
   
  //LikeFilter filter = new LikeFilter("memberOf","CN=DEVS1_DEV_Inhouse_SecGroup,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int");
   //OrFilter filter = new OrFilter();
   //filter.or(new EqualsFilter("memberOf","CN=DEVS1_DEV_Inhouse_SecGroup,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int"));
   //filter.or(new EqualsFilter("memberOf","CN=LOS_ADMIN_FULL,OU=Security_Groups_Distribution_Lists,DC=DevS1,DC=int"));
      //System.out.println("LDAP Query " + filter.encode());
      //ldapTemplate.setIgnoreSizeLimitExceededException(true);
      //ldapTemplate.setIgnorePartialResultException(true);
         //return ldapTemplate.search("",filter.encode(),new LDAPUSerAttributesMapper());
      
   
 }
 
 
 public List<String> getAllPersonNames() {
       return ldapTemplate.search(
          query().where("objectclass").is("user"),
          new AttributesMapper<String>() {
             public String mapFromAttributes(Attributes attrs)
                throws NamingException {
                return (String) attrs.get("cn").get();
             }
          });
   }
 
 /*
  This is how you map your results back to a 
  Java object
 
 */
  private class LDAPUSerAttributesMapper implements AttributesMapper<LDAPUser> {
       public LDAPUser mapFromAttributes(Attributes attrs) throws NamingException {
       LDAPUser person = new LDAPUser();
          person.setsAMAccountName(getValue(attrs.get("sAMAccountName")));
          person.setName(getValue(attrs.get("name")));
          person.setGivenName(getValue(attrs.get("givenName")));
          person.setCanonicalName(getValue(attrs.get("canonicalName")));
          try {
       //Special handling to pull this property as it can  be one to many.
    person.setMemberOfValueList(getAllValues(attrs.get("memberOf")));
   } catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
          return person;
       }
    }
  
  /**
   * Checks for null and accordingly gets String value of the attribute.
  * @param attr
  * @return
  * @throws NamingException
  */
 private static String getValue(Attribute attr)throws NamingException{
   if((attr!= null) &&(attr.get() != null)){
     return (String)attr.get();
   }
    return "";
  }
 
 private static void dumpAll(Attributes attributes) throws NamingException {
  NamingEnumeration<? extends Attribute>  allAttr= attributes.getAll();
  while (allAttr.hasMoreElements()) {
   Attribute attribute = (Attribute) allAttr.nextElement();
   System.out.println("All="+ attribute.get().toString());
  }
  
 }
 
 
 public static List<String> getAllValues(Attribute attribute)throws Exception {
  List<String> valueList = new ArrayList<String>();
  if(attribute != null){
   NamingEnumeration<?>  childAttr= attribute.getAll();
   while(childAttr.hasMoreElements()){
    Object obj = childAttr.next();
    //System.out.println("id="+ attribute.getID()+" Value="+ obj.toString()+"\n");
    valueList.add(obj.toString());
   }
  }
  return valueList;
 }
 
 public static void main(String[] args) {
          Resource resource = new ClassPathResource("/spring-config.xml");
          BeanFactory factory = new XmlBeanFactory(resource);
          LDAPUserRepo ref = (LDAPUserRepo) factory.getBean("ldapUserRepoBean");
          System.out.println(ref.getAllPersonNameObjs());
          System.out.println("Done.");
 }
}

As shown the ldaptemplate is much easier and has more flexibility so that we can use many filters, nest the filters to restrict data from  LDAP active directory. So needless to say Spring approach is much easier.


No comments:

Post a Comment