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.
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:
public LDAPSearch(){
if (IsLDAPAvailable())
{
DirectoryEntry root = new DirectoryEntry("LDAP://RootDSE");
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
var ldap = new LDAPSearch();
var props = new List<LDAPSearch.DirectoryProperty>();
props.Add(LDAPSearch.DirectoryProperty.SAMACCOUNTNAME);
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 enum
s - [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
enum
s 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.
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);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER,
LDAPSearch.SearchBy.EMAIL, "mike_direnzo@hotmail.com", props);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER,
LDAPSearch.SearchBy.AD_LOGIN_ID, "OU812", props);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER,
LDAPSearch.SearchBy.CNNAME, "Mike Direnzo", props);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.USER,
LDAPSearch.SearchBy.EID, "XXXXXXX", props);
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.
props.Add(LDAPSearch.DirectoryProperty.MAIL);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.GROUP,
LDAPSearch.SearchBy.AD_LOGIN_ID,"AD_GROUP_1",props);
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.
props.Add(LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME);
props.Add(LDAPSearch.DirectoryProperty.NAME);
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.
props.Add(LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME);
props.Add(LDAPSearch.DirectoryProperty.NAME);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.PRINTER,
LDAPSearch.SearchBy.PRINTERNAME,"PS*",props);
var toReturn = ldap.LDAPSearch(LDAPSearch.SearchTarget.PRINTER,
LDAPSearch.SearchBy.PRINTERNAME,"PS100100",props);
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.
var isUserLogonAccountLocked = ldap.GetValue(LDAPSearch.DirectoryProperty.LOCKOUTTIME,
toReturn);
var message = string.Format("Account: {0} locked: {1}",
ldap.GetValue(LDAPSearch.DirectoryProperty.SAMACCOUNTNAME, toReturn),
(System.Convert.ToInt32(isUserLogonAccountLocked) > 0));
var t = ldap.GetValue(LDAPSearch.DirectoryProperty.MEMBEROF,
toReturn, "AD_GROUP_1");
var computerName = ldap.GetValue
(LDAPSearch.DirectoryProperty.DISTINGUISHEDNAME, toReturn);
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