ARTICLE

.Net and active directory

Posted by sathya Articles | Active Directory in VB.NET April 23, 2008
This article outlines connecting to AD from .Net using ADO and System.DirectoryServices and also forming a unique ID from AD, moving entries from AD to a database and forming AD queries (also date formatting for AD).
 
Reader Level:

Introduction

When we had the requirement to read the active directory from .Net, for a project, we were left with many questions and grey areas on mind; sadly there was not much info available on googling. After a thorough study by creating many proof of concepts we got a bit comfortable with the same, I thought why not put it all onto the web for the use of any people who want to access the Active directory through .Net. 

Here I will discuss the multiple options available to connect to active directory, and also the patterns of AD queries, and other small findings that I came across while doing all this.

LDAP and GC

Now why do we need to know these two jargons? - Simply because these are the two ways you can connect to the active directory. For the purpose of correct documentation the abbreviations for the two are given below.

LDAP - Lightweight directory access protocol.
GC - Global Catalog.

The conventional definition for LDAP is as follows:

  • A set of protocols for accessing information directories

The LDAP is a full replica of a single domain and that "GC:" is a partial replica of all domains in the forest.

The global catalog has a database table like structure which helps in faster searches. The Global Catalog contains directory data for all domains in a forest. The Global Catalog contains a partial replica of every domain directory. It contains an entry for every object in the enterprise forest, but does not contain all the properties of each object. Instead, it contains only the properties specified for inclusion in the Global Catalog. The global catalog is created by replicating from all the domains in a forest of the active directory on a periodical basis.

An LDAP and GC path would look something like these.

  • "LDAP://<host name>/<object name>"
  • "GC://<host name>/<object name>"

In the examples above, "LDAP:" specifies the LDAP provider. "GC:" uses the LDAP provider to bind to the Global Catalog service to execute fast queries.

If you were well accustomed with how the active directory is set up, the above lines would give you a clear insight on what a GC and LDAP is. For an overview on the Active directory structure (what is a forest? what is a domain? etc) please refer http://www.windowsitlibrary.com/Content/155/07/toc.html.

Having known what are the two different ways the AD (hereon called AD instead of Active directory), allows itself to be contacted, lets look at how our other party ".NET" can interact with Active Directory and before that how to query AD.

Forming Queries for Active directory

The LDAP search strings used to query Active directory is a little different from the normal SQL queries we would write on databases.

These queries are based on one or more of the key attributes as follows

ObjectCategory

This could be 'user' or 'printer' or any defined category in the AD. If you would be searching only users then this value needs to be set to user e.g (objectCategory=user). By specifying this search narrows down, and you can expect to see results sooner.

AD attributes

There is a big list of fields that can be used in Activedirectory, apart from the extensive set it provides, ad administrators can add their own fields. The query can consist of any of the named fields of AD.e.g. (samAccountname=john.abraham).

To combine the criteria, the normal bitwise operators (&, |, !) can be used,

For example, if I want to query for all the users whose distinguishedName begin with 'john' my query would look like this (&(objectCategory=user)(cn=john*))

If I wanted to find how many users whose names begin either with jack or jill this is how I would frame my query.
(&(objectCategory=user)(|(cn=jack*)(cn=jill*))).

Users whose mailed is empty.
(&(objectCategory=user)(!(mail=*)))

For more such examples you could refer

http://www.petri.co.il/ldap_search_samples_for_windows_2003_and_exchange.htm.
http://www.microsoft.com/technet/scriptcenter/guide/sas_ads_emwf.mspx?mfr=true

For specifying this search you need to be aware of the available properties/fields in your active directory.

So much for queries, but then if you want to involve dates in your query, there is bit more of a job to be done, i.e. AD doesn't accept our normal date format for the queries, so the date needs to be converted into a AD readable format. Pls find below a function to do the same.

'<summary>

'Method to convert date to AD format

'</summary>

'<param name="date">date to convert</param>

'<returns>string containing AD formatted date</returns>

Private Shared Function ToADDateString(ByVal [date] As DateTime) As String

    Dim year As String = [date].Year.ToString()

    Dim month As Integer = [date].Month

    Dim day As Integer = [date].Day

    Dim hr As Integer = [date].Hour

    Dim sb As String = String.Empty

    sb += year

    If month < 10 Then

        sb += "0"

    End If

    sb += month.ToString()

    If day < 10 Then

        sb += "0"

    End If

    sb += day.ToString()

    If hr < 10 Then

        sb += "0"

    End If

    sb += hr.ToString()

    sb += "0000.0Z"

    Return sb.ToString()

End Function

Another challenge was to find a unique ID from AD which can be used as a primary key in cases when we need to take a snapshot of the AD to the database. You might wonder that the email id or aliasname of a person should be unique, but in certain organizations when  request for an extra mailbox or any service account is raised, the additional account is created in the same name as the existing one, thus making those attributes unusable as unique Ids.

In AD the unique id used is the objectGUID which cannot be directly inserted into the database as it's a 128 bit octet string. To store this into a Database it needs be converted to a readable format for example a binary string. Find below a code snippet that would do just that.

Dim arraybyte As Byte() = CByte(de.Properties("objectGUID").Value)

Dim OctetToHexStr As New StringBuilder()

For k As Integer = 0 To arraybyte.Length - 1

    OctetToHexStr.Append("\" + Convert.ToString(Convert.ToByte(arraybyte(k)), 16))

Next

ADO and System.DirectoryServices

From .NET there are two that you can connect to AD, one is through our good old ADO, which we have used from age-old days, and the other is through the .Net provided namespace System.DirectoryServices.

By using ActiveX data objects (or ADO as its more popularly known) you can connect to AD, as you would with any other database. The connection provider you would use to do so is "ADsDSOObject".

The other basic objects that you would require to establish a connection to AD and query it would be the connection object, recordset object and lastly the command object to maintain active connections, specify query parameters such as page size, search scope and so on.

Since our focus here is using System.DirectoryServices, we will leave the ADO part aside. But just as an intro, find below the code to establish connection through ADO.

objConnection = CreateObject("ADODB.Connection")

objConnection.Provider = "ADsDSOObject"

objConnection.Properties("User ID").Value = "myUser"

objConnection.Properties("Password").Value = "myPassword"

objConnection.Properties("Encrypt Password") = True

objConnection.Open("Active Directory Provider")

Now that we are done with creating a connection object, we can proceed on to create a command object to query AD. The code snippet to create a command object for the above given connection object would be as follows.

Dim objCommand As New ADODB.Command()

objCommand.ActiveConnection = objConnection

strBase = "<LDAP://OU=User Directory,DC=asia,DC=myDomain,DC=com>"

Having set the connection, we are now left with the task of reading the AD. Now we need to set a query to read the AD, just as we would specify a Sql for a database, a query needs to be passed to the active directory for it to pick the objects that match the query.

Suppose I want the list of users created after a certain date say strDate, then my AD query would look something like this.

Dim strFilter As String = "(&(objectCategory=user)(objectClass=user)(whenCreated>=" + strFromDate + "))"

The following is sample code to do the same.

Dim rsAD As ADODB.Recordset = New ADODB.RecordsetClass()

Try

    rsAD.Open(strFilter, adConn, ADODB.CursorTypeEnum.adOpenForwardOnly, ADODB.LockTypeEnum.adLockReadOnly, 0)

Catch exp As Exception

    Response.Write(exp.Message)

    Response.[End]()

End Try

Dim userDataTable As New DataTable()

userDataTable.Columns.Add("AccountName")

userDataTable.Columns.Add("CommonName")

userDataTable.Columns.Add("CreatedDate")

While Not rsAD.EOF

Dim newRow As DataRow = userDataTable.NewRow()

    newRow(0) = rsAD.Fields(0).Value

    newRow(1) = rsAD.Fields(1).Value

    newRow(2) = rsAD.Fields(2).Value

    userDataTable.Rows.Add(newRow)

    rsAD.MoveNext()

End While

Having filled up our Datatable with the records from AD, we can choose to display it in any format (DataList, DataReader or Datagrid).

System.DirectoryServices

In this namespace the most used classes are the DirectoryEntry and DirectorySearcher.

As the name suggests DirectoryEntry would represent each entry of the AD, be it a user or a printer or any such resource.

The DirectorySearcher class helps in querying the AD. Please follow the inline comments for details on code.

Imports System

Imports System.Collections

Imports System.DirectoryServices

Imports System.Data

Imports System.Security.Permissions

Imports System.IO

Imports System.Text

<Assembly: SecurityPermission(SecurityAction.RequestMinimum, Unrestricted:=True)>

Namespace Web.Apps.ADInterface

    ''' <summary>

    ''' Class to interface with AD and search for new, modified and deleted users.

    ''' </summary>

    Public Class ADSearch

#Region "Private Variables"

        Private Shared _gcPath As String = "GC://mydomain.com"

        Private Shared _serviceAccountName As String = "Europe\abcsdfs-S"

        Private Shared _servicePassword As String = "2$%^&*()"

        Private entry As New DirectoryEntry()

#End Region

#Region "Constructor"

        Private Sub New()

            entry.Path = _gcPath

            entry.Username = _serviceAccountName

            entry.Password = _servicePassword

        End Sub

#End Region

#Region "Methods"

        ''' <summary>

        ''' Method to Search for new,Modified and Deleted users

        ''' </summary>

        ''' <param name="createdDate"></param>

        Public Shared Sub SearchADUsers(ByVal createdDate As DateTime, ByVal path As String)

            Dim strFilter As String = String.Empty

            Dim strFromDate As String = ToADDateString(Convert.ToDateTime(createdDate))

 

            'Search criteria for fetching users whose account name, mail and distinguished name are not empty and whose entries are changed since the specified date (either created or modified after the specified date)

 

            strFilter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*)"

                        (|(whenChanged>=" + strFromDate + ")(whenCreated>=" + strFromDate + ")))"

 

            ADSearchUsers(strFilter, path)

        End Sub

        ''' <summary>

        ''' Method to Search for new,Modified and Deleted users

        ''' </summary>

        Public Shared Sub TakeADSnapshot()

            Dim filter As String = String.Empty

            filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))"

            ADSnapshot(filter, "C:\insert.CSV")

        End Sub

 

        ''' <summary>

        ''' Method to Search for new,Modified and Deleted users

        ''' </summary>

        ''' <param name="path">CSV file Path</param>

        Public Shared Sub TakeADSnapshot(ByVal path As String)

            Dim filter As String = String.Empty

            filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))"

            ADSnapshot(filter, path)

        End Sub

        'The function below takes a snapshot of AD users who satisfy the specified criteria and constructs a CSV file out of it, This is done it's the easiest way to move it into a database.

        ''' <summary>

        ''' Method to get take an AD snapshot

        ''' </summary>

        ''' <param name="filterString">AD Search string</param>

        ''' <param name="path">Path of CSV file</param>

        Private Shared Sub ADSearchUsers(ByVal filterString As String, ByVal path As String)

 

            Dim entry As New DirectoryEntry()

            entry.Path = _gcPath

            entry.Username = _serviceAccountName

            entry.Password = _servicePassword

 

            Dim mySearcher As New DirectorySearcher(entry)

            mySearcher.Filter = filterString.ToString()

            Dim tw As TextWriter = New StreamWriter(path, True)

            mySearcher.PageSize = 10

            mySearcher.CacheResults = False

            Dim sqlinsert As StringBuilder = Nothing

 

            'Add all properties that need to be fetched

            mySearcher.PropertiesToLoad.Add("displayName")

            mySearcher.PropertiesToLoad.Add("givenname")

 

 

            mySearcher.PropertiesToLoad.Add("sn")

            mySearcher.PropertiesToLoad.Add("ou")

            mySearcher.PropertiesToLoad.Add("employeeType")

            mySearcher.PropertiesToLoad.Add("mail")

            mySearcher.PropertiesToLoad.Add("telephoneNumber")

            mySearcher.PropertiesToLoad.Add("samAccountName")

            mySearcher.PropertiesToLoad.Add("whenCreated")

            mySearcher.PropertiesToLoad.Add("whenChanged")

            mySearcher.PropertiesToLoad.Add("objectGUID")

            mySearcher.PropertiesToLoad.Add("c")

 

            'The search scope specifies how deep the search needs to be, it can be either "base"- which means only in the current //level, and "OneLevel" which means the base and one level below and then "subtree"-which means the entire tree needs //to be searched.

 

            mySearcher.SearchScope = SearchScope.Subtree

            Dim resultUsers As SearchResultCollection = mySearcher.FindAll()

            Dim fpos As Integer, spos As Integer

            Dim dn As String, newdn As String, newerdn As String

            For Each srUser As SearchResult In resultUsers

                Try

                    Dim de As DirectoryEntry = srUser.GetDirectoryEntry()

                    Dim arraybyte As Byte() = CByte(de.Properties("objectGUID").Value)

                    Dim OctetToHexStr As New StringBuilder()

                    For k As Integer = 0 To arraybyte.Length - 1

                        OctetToHexStr.Append("\" + Convert.ToString(Convert.ToByte(arraybyte(k)), 16))

                    Next

 

                    dn = de.Properties("distinguishedName")(0).ToString()

                    sqlinsert = New StringBuilder()

                    'To get the domain name from Distinguished name

                    fpos = dn.IndexOf("DC=", 0)

                    newdn = dn.Substring(fpos, dn.Length - fpos)

                    spos = newdn.IndexOf(",DC=", 3)

                    newdn = newdn.Substring(0, spos)

                    newerdn = newdn.Substring("DC=".Length, newdn.Length - 3)

                    sqlinsert.Append(OctetToHexStr.ToString())

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("givenname").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("sn").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("ou").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("employeeType").Value)

                    sqlinsert.Append(";")

 

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("mail").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("samAccountName").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("c").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("l").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(Convert.ToDateTime(de.Properties("whenChanged")(0).ToString().TrimEnd()).ToString("dd-MMM-yyyy"))

                    sqlinsert.Append(";")

                    sqlinsert.Append(Convert.ToDateTime(de.Properties("whenCreated")(0).ToString().TrimEnd()).ToString("dd-MMM-yyyy"))

                    sqlinsert.Append(";")

                    AddressOf

                    sqlinsert.Append(DateTime.Now.ToString("dd-MMM-yyyy"))

                    sqlinsert.Append(";")

                    sqlinsert.Append(newerdn)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("legacyExchangeDN").Value)

                    sqlinsert.Append(";")

                    sqlinsert.Append(de.Properties("distinguishedName").Value)

                    'sqlinsert = OctetToHexStr+ ";" + de.Properties["givenname"].Value + ";" +

                    de.Properties("sn").Value(+";" + de.Properties("ou").Value + ";" + de.Properties("employeeType").Value + ";" + +";" + de.Properties("mail").Value + ";" + de.Properties("samAccountName").Value + ";" + de.Properties("c").Value + ";" + de.Properties("l").Value + ";" + Convert.ToDateTime(de.Properties("whenChanged")(0).ToString().TrimEnd()).ToString("dd-MMM-yyyy") + ";" + Convert.ToDateTime(de.Properties("whenCreated")(0).ToString().TrimEnd()).ToString("dd-MMM-yyyy") + ";" + DateTime.Now.ToString("dd-MMM-yyyy") + ";" + newerdn + ";" + de.Properties("legacyExchangeDN").Value + ";" + de.Properties("distinguishedName").Value)

                    de.Close()

                    tw.WriteLine(sqlinsert)

                    sqlinsert.Remove(0, sqlinsert.Length)

                Catch

                    Throw

                End Try

            Next

            tw.Close()

        End Sub

#End Region

    End Class

End Namespace

Conclusion

In conclusion I would like to highlight upon some points, it would be useful to keep them in mind while working on Active Directory.

  • Active directory searches would be pretty slow compared to database searches, so it's imperative to narrow down the search criteria as much as possible.
  • To search for deleted users in Active directory could be quite a challenge as the deleted items are physically moved to the obsolete users directory and after a certain "tombstone" period will be permanently deleted. But organisations follow some pattern of identifying deleted user's by certain means like suffixing the samAccountname with-Deleted or prefixing the username with a "_" and so on. Before you do a search on deleted users, it would be worthwhile to consult your AD administrator to know the convention followed.
  • There are much more properties attached to the directorysearcher and directoryentry classes, it would be worthwhile to go through them in msdn.
  • This paper intends to present an insight of connecting to AD using .net, and the code used here are only snippets and not fully working solutions.
  • The class DirectorySearcher would give only a readonly snapshot of AD, to do modifications on the AD you would have to follow different pattern, which is out of scope in this document.
  • Of the methods mentioned using System.DirectoryServices is better than using ADO in .net, nevertheless ADO can be used from VB or ASP.

Happy programming!!!

NOTE: THIS ARTICLE IS CONVERTED FROM C# TO VB.NET USING A CONVERSION TOOL. ORIGINAL ARTICLE CAN BE FOUND ON C# Corner (http://www.c-sharpcorner.com/).

Login to add your contents and source code to this article
share this article :
post comment
 
6 Months Free & No Setup Fees ASP.NET Hosting!
Become a Sponsor
PREMIUM SPONSORS
  • Finally – a virtual platform that delivers next-generation Windows Server 2008 Hyper-V virtualization technology from a managed hosting partner you can truly depend on. Visit www.maximumasp.com/max for a FREE 30 day trial. Hurry offer ends soon. Climb aboard the MaxV platform and take advantage of High Availability, Intelligent Monitoring, Recurrent Backups, and Scalability – with no hassle or hidden fees. As a managed hosting partner focused solely on Microsoft technologies since 2000, MaximumASP is uniquely qualified to provide the superior support that our business is built on. Unparalleled expertise with Microsoft technologies lead to working directly with Microsoft as first to offer IIS 7 and SQL 2008 betas in a hosted environment; partnering in the Go Live Program for Hyper-V; and product co-launches built on WS 2008 with Hyper-V technology.
    Get 2 Months Free of ASP.NET Hosting for Only $4.95/month! Receive FREE MS SQL and MySQL Databases Including ASP.NET 4/3.5, MVC 3.0, Silverlight 4, Windows 2008/IIS 7.0 Plus FREE IIS 7 Modules. Host UNLIMITED ASP.NET Web Sites - Click Here!
6 Months Free & No Setup Fees ASP.NET Hosting!
Become a Sponsor