Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

LDAP Search Utility

5.00/5 (6 votes)
24 Oct 2017CPOL8 min read 38K   1.3K  
Single C# class file that can be dropped into any project and used right away
In this article, you will see an easy to use programmatic LDAP search utility, a single C# class file that can be dropped into any project and used right away.

LDAP Search Utility

Introduction

This is an easy to use programmatic LDAP search utility class that will work right of the box. It uses a serverless binding technique with an anonymous security context. It will work with most all Microsoft flavored active directories. Search the active directory for:

  • Users
  • Groups
  • Computers
  • Printers

Among other things, the class is flexible to allow the following:

  • Verify a users group member access
  • List the users of a group
  • Check the lock out status of a users active directory account
  • When was the last time a user logged on or off?

This utility can return specific attributes or all of them. The directory search filter implementation targets the category versus the class (see objectCategory vs. objectClass). This utility acts as a wrapper to System.DirectoryServices.DirectorySearcher.

Background

This utility is not the panacea solution as there are many ways to implement an LDAP search. A quick article search on LDAP within codeproject.com will prove it. The anonymous nature of the implementation of this utility's security context allows for searching of LDAP information in a simple flexible manner. I wanted to create an easier way of retrieving LDAP information. and be able to use it anywhere.

In a recent project, I needed to compare a user's title (ldap attribute) to a constant. If the title contained the constant, then the logic would branch accordingly. Another use case required the user to be a member of a certain active directory group - not to be confused with active directory membership provider class. If the user was a member of a given active directory group, the logic would branch accordingly. These kinds of searches are made easier to implement with this utility class. Moreover, this utility is a standalone class and can be dropped into any project and used immediately.

Using the Code

Serverless Binding

The serverless binding technique allows for extreme flexibility. Introduced in LDAP 3.0, RootDSE is defined as the root of the directory data tree on a directory server. The rootDSE is not part of any namespace. The purpose of the rootDSE is to provide data about the directory server. Therefore, one does not need to know an LDAP address so it works, generally, for most Microsoft active directories. RootDSE stands for Root DSA (Directory Service Agent) Specific Entry, which is the root of the LDAP server. This entry is a pseudo object in the tree, which means it's an unnamed entry at the root of the tree. This utility class can be dropped into any project and immediately used without issue. The following is the source code of the constructor:

C#
public LDAPSearch(){
    if (IsLDAPAvailable()) //throws an exception if the directory is not available
    {
        DirectoryEntry root = new DirectoryEntry("LDAP://RootDSE");
        //root.Properties contain a list of properties 
        //that describe the directory server.  Eg. "defaultNamingContext"
        root = new DirectoryEntry
               ("LDAP://" + root.Properties["defaultNamingContext"][0]);
        this.DirectorySearcher = new DirectorySearcher(root);
    }
} 

The nature of LDAP searches has the propensity to return more than one value. This utility assumes that more than one result set can be returned. The return object is neatly packaged into a complex dictionary. For a given search, one needs to pass in the desired return properties. These properties get added to the DirectorySearcher.PropetiesToLoad collection. I use an Enum to make the implementation easier to use which are contained in the LDAPSearch.DirectoryProperty. Each LDAPSearch.DirectoryProperty enum contains a description attribute which matches the actual attribute within the directory. Out of the box, the source code contains a limited set of commonly used properties. It is common for a directory administrator to add custom properties for various use cases. You will need to expand the LDAPSearch.DirectoryProperty enum collection to suite your needs. For a complete list of Microsoft oriented directory attributes, see Indexed Attributes on MSDN.

Constructor

C#
//.ctor
var ldap = new LDAPSearch(); 

//optionally add some target properties to return from the result
var props = new List<LDAPSearch.DirectoryProperty>();
props.Add(LDAPSearch.DirectoryProperty.SAMACCOUNTNAME);

//invoke the search method
var toReturn = ldap.LDAPSearch([SEARCH TARGET], [SEARCH BY], 
    ["SEARCH KEY"],[0..N PROPERTIES TO SEARCH FOR], [BEGIN RANGE], [END RANGE]);
  • [SEARCH TARGET]: enum LDAPSearch.SearchTarget
  • [SEARCH BY]: enum LDAPSearch.SearchBy
  • ["SEARCH KEY"]: This is the value of what is being searched for
  • [0..N PROPERTIES TO SEARCH FOR]: a collection of LDAPSearch.DirectoryProperty enums
  • [BEGIN RANGE]: Optional and can be omitted
  • [END RANGE]: Optional and can be omitted
    Note: [BEGIN RANGE], [END RANGE] ranges are used when [SEARCH TARGET] = LDAPSearch.SearchTarget.GROUP and are optional

Return Object - Dictionary<int, List<KeyValuePair<string, List<string>>>>

Given the nature of LDAP directory searches, by default, the results are buried in a System.DirectoryServices.SearchResultCollection object when using System.DirectoryServices.DirectorySearcher.FindAll() (see MSDN). It is also possible for more than one result set to be returned. For example, when searching a user by employee ID, there may be more than one active directory login account associated. Some may find pulling values out this collection difficult. This utility simplifies the effort by dumping the results into an indexed complex dictionary. Retrieving values by key in one line of code makes the process very easy and clean. Moreover, traversing the dictionary using LINQ/Lambda makes it easier as well.

NOTE [0..N PROPERTIES TO SEARCH FOR] parameter is an enum collection of type LDAPSearch.DirectoryProperty whose description attribute matches the exact spelling of the properties contained within the active directory that are available. Not every active directory will contain the same attributes. I have included a basic set of LDAPSearch.DirectoryProperty enums with description attributes that are most common. One can expand this to fit their need.

User Searches

The utility allows for searching a user in active directory by a variety of means.
NOTE: props parameter. props is a LDAPSearch.DirectoryProperty enum collection that contains 0 or more desired attributes to be returned.

C#
//Load up some properties to return
props.Add(LDAPSearch.DirectoryProperty.LOCKOUTTIME);
props.Add(LDAPSearch.DirectoryProperty.LASTLOGOFF);
props.Add(LDAPSearch.DirectoryProperty.LASTLOGON);
props.Add(LDAPSearch.DirectoryProperty.SAMACCOUNTNAME);
props.Add(LDAPSearch.DirectoryProperty.USERACCOUNTCONTROL);

//By Email
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER, 
               LDAPSearch.SearchBy.EMAIL, "mike_direnzo@hotmail.com", props); 

//By Active Directory ID (VH)
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER, 
               LDAPSearch.SearchBy.AD_LOGIN_ID, "OU812",  props); 

//By CNAME
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER, 
               LDAPSearch.SearchBy.CNNAME, "Mike Direnzo", props);

//By Employee ID
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER, 
               LDAPSearch.SearchBy.EID, "XXXXXXX", props); 

//Wild Card - returns all EIDs that start with 11
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER, 
               LDAPSearch.SearchBy.EID, "11*", props); 

Group Searches

The utility allows for searching an active directory group via changing the search target to: LDAPSearch.SearchTarget.GROUP. The search result will return a special property by default called member;range=0-1499. It contains all the active directory users and/or entities who are members of the group. In some active directory server controller configurations, the maximum number of objects returned is 1500. This is controlled by the range attribute. This utility abstracts the implementation aspect in that only a range needs to be provided - and it is optional. When not provided, it defaults to 0-1499. The member;range=0-1499 property will contain all the active directory members and it potentially can be a lot. So one can page the entire membership of a given active directory group by calling the search method several times while supplying subsequent contiguous ranges.
NOTE: props parameter. props is an enum collection that contains 0 or more desired attributes to be returned.

C#
props.Add(LDAPSearch.DirectoryProperty.MAIL);

//omitting range search will return 0-1499 results 
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.GROUP, 
               LDAPSearch.SearchBy.AD_LOGIN_ID,"AD_GROUP_1",props);

//using a range search
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.GROUP, 
               LDAPSearch.SearchBy.AD_LOGIN_ID,"AD_GROUP_1",props, 100, 199);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.GROUP, 
               LDAPSearch.SearchBy.AD_LOGIN_ID,"AD_GROUP_1",props, 200, 299);

Computer Name Searches

The utility allows for searching a computer name via changing the search target to: LDAPSearch.SearchTarget.COMPUTER. This can be useful if one needs to see what OU the computer is in. For example, group policies within an active directory are applied by OU and the LDAP path it is contained in. Troubleshooting can be made a lot easier when one knows the precise OU a computer is contained in. Obviously, this can be applied to users, groups, printers, and other as well.
NOTE: props parameter. props is a LDAPSearch.DirectoryProperty enum collection that contains 0 or more desired attributes to be returned.

C#
props.Add(LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME);
props.Add(LDAPSearch.DirectoryProperty.NAME);

//COMP_1001001 (Rush SOS)
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.COMPUTER, 
               LDAPSearch.SearchBy.COMPUTERNAME,"COMP_1001001",props);

Printer Name Searches

The utility allows for searching a printer via changing the search target to: LDAPSearch.SearchTarget.PRINTER. This can be useful if one needs to see what OU the printer is in. For example, group policies within an active directory are applied by OU and the LDAP path it is contained in. Troubleshooting can be made a lot easier when one knows the precise OU a printer is contained in.
NOTE: props parameter. props is a LDAPSearch.DirectoryProperty enum collection that contains 0 or more desired attributes to be returned.

C#
props.Add(LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME);
props.Add(LDAPSearch.DirectoryProperty.NAME);

//wild card
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.PRINTER, 
               LDAPSearch.SearchBy.PRINTERNAME,"PS*",props);

//specific
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.PRINTER, 
               LDAPSearch.SearchBy.PRINTERNAME,"PS100100",props);

Retrieving Values

This utility dumps the directory search result into a complex object: Dictionary<int, List<KeyValuePair<string, List<string>>>>. Retrieving an item from the result occurs in one line of code. Of course, the one dependency is that when retrieving an item from the result set, said item needs to have been in the props collection (an enum collection of type LDAPSearch.DirectoryProperty). Otherwise an exception is thrown.

The utility exposes a method with two overloads:

  • var stringValue = ldap.GetValue(LDAPSearch.DirectoryProperty.MEMBEROF, toReturn) to return a simple value by key.
  • var booleanValue = ldap.GetValue(LDAPSearch.DirectoryProperty.MEMBEROF, toReturn, "AD_GROUP_1")
    NOTE: toReturn contains the result of a search
    This overload returns a boolean and is meant for checking for the existence of a group name value in a LDAPSearch.DirectoryProperty.MEMBEROF property which is a collection of type String. No doubt, this can be expanded or refactored in future versions.
C#
//User 
var isUserLogonAccountLocked = ldap.GetValue(LDAPSearch.DirectoryProperty.LOCKOUTTIME, 
                               toReturn); //returns 0 or 1
var message = string.Format("Account: {0} locked: {1}",
        ldap.GetValue(LDAPSearch.DirectoryProperty.SAMACCOUNTNAME, toReturn),
		(System.Convert.ToInt32(isUserLogonAccountLocked) > 0));

//Group
var t = ldap.GetValue(LDAPSearch.DirectoryProperty.MEMBEROF,
                      toReturn, "AD_GROUP_1"); //returns bool

//Computer
var computerName = ldap.GetValue
                   (LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME, toReturn);

//Printer
var printerName = ldap.GetValue
                  (LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME, toReturn);

Compatibility Issues

Have you ever downloaded a code snippet or project only to find out it is not compatible with your version of .NET Framework or Visual Studio?

.NET Version Compatibility

The utility can use .NET Framework versions 4.0 - 4.6.x as is without modification. There is a method that allows for named and optional arguments which were introduced in .NET 4.0. Also I am using an anonymous delegate to return the search results (introduced in .NET 3.0). To my knowledge and as of this writing, it is not compatible with .NET CORE 1.0 or .NET 2.0 (yet) because of the lack of inclusion of System.DirectoryServices class. This was built using Visual Studio 2015 .NET Framework 4.6 and successfully implemented.

Known Compatible Active Directory Versions

AD DS Version
Windows Server 2008 30
Windows Server 2008 R2 31
Windows Server 2012 31

History

  • 21st October, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)