from sathya:.NET and Active Directory in c-sharpcorner.com (19/04/2007)
1. Two jargons:
GC - Global Catalog: a partial replica of all domains in the forest.
- The GC has a database table like structure which helps in faster searches.
- The GC contains directory data for all domains in a forest, but does not contain all the properties of each object. Instead, it contains only the properties specified for inclusion in GC.
LDAP - Lightweight directory access protocol: a full replica of a single domain.
A 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.
2. Forming queries for AD
The LDAP search strings used to query AD is a little different from the normal SQL queries on DB. These queries are based on one or more of the key attributes as follows.
2.1 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. (like table in DB?)
2.2 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). (like where condition in DB)
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.
2.3 AD date format
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 static string ToADDateString(DateTime date)
{
string year = date.Year.ToString();
int month = date.Month;
int day = date.Day;
int hr = date.Hour;
string sb = string.Empty;
sb += year;
if (month < 10)
{
sb += "0";
}
sb += month.ToString();
if (day < 10)
{
sb += "0";
}
sb += day.ToString();
if (hr < 10)
{
sb += "0";
}
sb += hr.ToString();
sb += "0000.0Z";
return sb.ToString();
}
2.4 Unique ID - objectGUID
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 converted to a readable format for example a binary string. Find below a code snippet that would do just that.
byte[] arraybyte = (byte[])de.Properties["objectGUID"].Value;
StringBuilder OctetToHexStr = new StringBuilder();
for (int k = 0; k < arraybyte.Length; k++)
{
OctetToHexStr.Append(@"\" + Convert.ToString(Convert.ToByte(arraybyte[k]),16));
}
3. Accessing AD from .NET in 2 ways
3.1 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");
ADODB.Command objCommand = new ADODB.Command();
objCommand.ActiveConnection = objConnection;
strBase = "<LDAP://OU=User Directory,DC=asia,DC=myDomain,DC=com>";
string strFilter ="(&(objectCategory=user)(objectClass=user)(whenCreated>=" + strFromDate +"))";
The following is sample code to do the same.
ADODB.Recordset rsAD = new ADODB.RecordsetClass();
try
{
rsAD.Open(strFilter,adConn,ADODB.CursorTypeEnum.adOpenForwardOnly,ADODB.LockTypeEnum.adLockReadOnly,0);
}
catch (Exception exp)
{
Response.Write(exp.Message);
Response.End();
}
DataTable userDataTable = new DataTable();
userDataTable.Columns.Add ("AccountName");
userDataTable.Columns.Add ("CommonName");
userDataTable.Columns.Add ("CreatedDate");
while(!rsAD.EOF)
{
DataRow newRow = 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();
}
3.2 System.DirectoryServices
using System;
using System.Collections;
using System.DirectoryServices;
using System.Data;
using System.Security.Permissions;
using System.IO;
using 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 static string _gcPath = "GC://mydomain.com";
private static string _serviceAccountName = @"Europe\abcsdfs-S";
private static string _servicePassword = "2$%^&*()";
private DirectoryEntry entry = new DirectoryEntry();
#endregion
#region Constructor
private ADSearch()
{
entry.Path = _gcPath;
entry.Username = _serviceAccountName;
entry.Password = _servicePassword;
}
#endregion
#region Methods
/// <summary>
/// Method to Search for new,Modified and Deleted users
/// </summary>
/// <param name="createdDate"></param>
public static void SearchADUsers(DateTime createdDate, string path)
{
string strFilter = string.Empty;
string strFromDate = 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);
}
/// <summary>
/// Method to Search for new,Modified and Deleted users
/// </summary>
public static void TakeADSnapshot()
{
string filter = string.Empty;
filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))";
ADSnapshot(filter, @"C:\insert.CSV");
}
/// <summary>
/// Method to Search for new,Modified and Deleted users
/// </summary>
/// <param name="path">CSV file Path</param>
public static void TakeADSnapshot(string path)
{
string filter = string.Empty;
filter += "(&(objectCategory=user)(samAccountName=*)(mail=*)(distinguishedName=*))";
ADSnapshot(filter, path);
}
//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 static void ADSearchUsers(string filterString, string path)
{
DirectoryEntry entry = new DirectoryEntry();
entry.Path = _gcPath;
entry.Username = _serviceAccountName;
entry.Password = _servicePassword;
DirectorySearcher mySearcher = new DirectorySearcher(entry);
mySearcher.Filter = filterString.ToString();
TextWriter tw = new StreamWriter(path, true);
mySearcher.PageSize = 10;
mySearcher.CacheResults = false;
StringBuilder sqlinsert = null;
//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;
SearchResultCollection resultUsers = mySearcher.FindAll();
int fpos, spos;
string dn, newdn, newerdn;
foreach (SearchResult srUser in resultUsers)
{
try
{
DirectoryEntry de = srUser.GetDirectoryEntry();
byte[] arraybyte = (byte[])de.Properties["objectGUID"].Value;
StringBuilder OctetToHexStr = new StringBuilder();
for (int k = 0; k < arraybyte.Length; k++)
{
OctetToHexStr.Append(@"\" + Convert.ToString(Convert.ToByte(arraybyte[k]), 16));
}
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(";");
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;
}
}
tw.Close();
}
#endregion
}
}
4. Conclusion
- 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. The class DirectorySearcher would give only a readonly snapshot of AD, to do modifications on the AD you would have to follow different pattern.