CodeProject; Tools & Code: Working With Active Directory In C#

Howto: (Almost) Everything In Active Directory via C#
Click title for source at CodeProject.com…
Introduction
When it comes to programmatically accessing Microsoft’s Active Directory a lot of people seem to have quite a difficult time tying all the pieces together to accomplish exactly what they want to. There are so many technologies available for communicating with LDAP that many programmers end up with a mix between COM+ ADSI calls and .NET class calls mixed into their code. ADSI code is so difficult to understand and follow that the creator of that code usually owns it for the entirety of it’s lifecycle since no one else wants to support it.
This article attempts to tie together the most commonly used elements involved in Active Directory Management in the simplest, most clean manner possible. I interact with Active Directory in nearly all of my applications (web & forms) and I have had to solve a lot of integration issues for many customers. When I was starting out with this technology I had a lot of growing pains so this is an attempt to help those programmers who may have a need to interact with the Directory but do not want to have to become an expert in the issue. However, certain rudimentary knowledge and concepts are required in order to utilize the code. You must be firmiliar with such terms as: distinguishedName, ldap paths, fully qualified domain names, object attributes (single string & multi-string), and general knowledge of ldap schemas.
Background
There is a great collection of sample code available on MSDN’s website for the v1.1 System.DirectoryServices assembly but there seems to be a void when it comes to the new functionality available in the v2.0 System.DirectoryServices.ActiveDirectory assembly.
Points of Concern
In order to communicate with Active Directory one must take into account network security, business rules, and technological constraints. This means that if you’re using Active Directory code from an ASP.NET page you must have impersonation enabled at the ASP.NET level as well as the IIS level. In addition if your Active Directory domain controller resides on a seperate physical server than the IIS then you’re going to need wrap your code around impersonation code since you’ll fall into the One-Hop Limitation.
You can find a great little impersonation class on theCodeProject.com
Alternatively, if you plan on running this code from a desktop assembly then you’re going to want to ensure that your machine is attached to a domain and can communicate with that domain. The impersonation is not necessary if the user running the code has sufficient priveledges on the domain.
Running Code In Batch Processes
It is also important to note that if you plan on running this code from an ASP.NET page in batch then you’re probably going to want to read up on ASP.NET multi-threading and the caveats involved in trying to impersonate a priveledged account in a multi-threaded environment. ASP.NET will time out on you if you try to run batch processes from it’s primary thread.
A Note On Method Parameters
You will notice that most of the methods require the same parameters. Rather than identify each time I will outline them now:
friendlyDomainName: the non qualified domain name (contoso – NOT contoso.com)
ldapDomain: the fully qualified domain such as contoso.com or dc=contoso,dc=com
objectPath: the fully qualified path to the object: CN=user,OU=USERS,DC=contoso,DC=com (same as objectDn)
objectDn: the distinguishedName of the object: CN=group,OU=GROUPS,DC=contoso,DC=com
userDn: the distinguishedName of the user: CN=user,OU=USERS,DC=contoso,DC=com
groupDn: the distinguishedName of the group: CN=group,OU=GROUPS,DC=contoso,DC=com
A Note on System.DirectoryServices.DirectoryEntry
You’ll notice in all the samples that we’re binding directly to the directoryEntry and not specifying a server or credentials. If you do not want to use an impersonation class you can send credentials directly into the DirectoryEntry constructor. The impersonation class is helpful for those times you want to use a static method and don’t want to go through the trouble of creating a DirectoryContext object to hold these details. Likewise you may want to target a specific domain controller.
Everywhere in the code that you see: LDAP:// you can replace with LDAP://MyDomainControllerNameOrIpAddress as well as every where you see a DirectoryEntry class being constructed you can send in specific credentials as well. This is especially helpful if you need to work on an Active Directory for which your machine is not a member of it’s forest or domain or you want to target a DC to make the changes to.
//Rename an object and specify the domain controller and credentials directly
public static void Rename(string server,
string userName,
string password,
string objectDn,
string newName)
{
DirectoryEntry child = new DirectoryEntry(“LDAP://” + server + “/” + objectDn, userName, password);
child.Rename(“CN=” + newName);
}
Managing Local Accounts With DirectoryEntry
It is important to note that you can execute some of these methods against a local machine as oppossed to an Active Directory if needed by simply replacing the LDAP:// string with WinNT:// as demonstrated below
//create new local account
DirectoryEntry localMachine = new DirectoryEntry(“WinNT://” + Environment.MachineName);
DirectoryEntry newUser = localMachine.Children.Add(“localuser”, “user”);
newUser.Invoke(“SetPassword”, new object[] { “3l!teP@$$w0RDz” });
newUser.CommitChanges();
Console.WriteLine(newUser.Guid.ToString());
localMachine.Close();
newUser.Close();
A few configuration changes need to be made to the code but it’s pretty straight forward. Below you can see an example of using DirectoryEntry to enumerate the members of the local “administrator” group.
DirectoryEntry localMachine = new DirectoryEntry(“WinNT://” + Environment.MachineName + “,Computer”);
DirectoryEntry admGroup = localMachine.Children.Find(“administrators”, “group”);
object members = admGroup.Invoke(“members”, null);
foreach (object groupMember in (IEnumerable)members)
{
DirectoryEntry member = new DirectoryEntry(groupMember);
Console.WriteLine(member.Name);
}
Active Directory Code
The code below is broken apart logically into usage categories. Again, this is not intended to be a complete library, just the code that I use on a daily basis.
Active Directory Management:
//These methods require these imports
//You must add a references in your project as well
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
Translate the friendly domain name to fully qualified domain:
public static string FriendlyDomainToLdapDomain(string friendlyDomainName)
{
string ldapPath = null;
try
{
DirectoryContext objContext = new DirectoryContext(
DirectoryContextType.Domain, friendlyDomainName);
Domain objDomain = Domain.GetDomain(objContext);
ldapPath = objDomain.Name;
}
catch (DirectoryServicesCOMException e)
{
{ldapPath = e.Message.ToString();
}
return ldapPath;
}
Enumerate Domains in the Current Forest
public static ArrayList EnumerateDomains()
{
ArrayList alDomains = new ArrayList();
Forest currentForest = Forest.GetCurrentForest();
DomainCollection myDomains = currentForest.Domains;
foreach (Domain objDomain in myDomains)
{
alDomains.Add(objDomain.Name);
}
return alDomains;
}
Enumerate Global Catalogs in the Current Forest
public static ArrayList EnumerateDomains()
{
ArrayList alGCs = new ArrayList();
Forest currentForest = Forest.GetCurrentForest();
foreach (GlobalCatalog gc in currentForest.GlobalCatalogs)
{
alGCs.Add(gc.Name);
}
return alGCs;
}
Enumerate Domain Controllers in a Domain
public static ArrayList EnumerateDomainControllers()
{
ArrayList alDcs = new ArrayList();
Domain domain = Domain.GetCurrentDomain();
foreach (DomainController dc in domain.DomainControllers)
{
alDcs.Add(dc.Name);
}
return alDcs;
}
Create a Trust Relationship
public void CreateTrust(string sourceForestName, string targetForestName)
{
Forest sourceForest = Forest.GetForest(new DirectoryContext(
DirectoryContextType.Forest, sourceForestName));
Forest targetForest = Forest.GetForest(new DirectoryContext(DirectoryContextType.Forest,
targetForestName));
// create an inbound forest trust
sourceForest.CreateTrustRelationship(targetForest,
TrustDirection.Outbound);
}
Delete a Trust Relationship
public void DeleteTrust(string sourceForestName, string targetForestName)
{
Forest sourceForest = Forest.GetForest(new DirectoryContext(
DirectoryContextType.Forest, sourceForestName));
Forest targetForest = Forest.GetForest(new DirectoryContext(DirectoryContextType.Forest, targetForestName));
// delete forest trust
sourceForest.DeleteTrustRelationship(targetForest);
}
Enumerate Objects in an OU
The parameter OuDn is the Organizational Unit distinguishedName such as OU=Users,dc=myDomain,dc=com Collapse
public ArrayList EnumerateOU(string OuDn)
{
ArrayList alObjects = new ArrayList();
try
{
DirectoryEntry directoryObject = new DirectoryEntry(“LDAP://” + OuDn);
foreach (DirectoryEntry child in directoryObject.Children)
{
string childPath = child.Path.ToString();
alObjects.Add(childPath.Remove(0,7)); //remove the LDAP prefix from the path
child.Close();
child.Dispose();
}
directoryObject.Close();
directoryObject.Dispose();
}
catch (DirectoryServicesCOMException e)
{
Console.WriteLine(“An Error Occurred: ” + e.Message.ToString());
}
return alObjects;
}
Enumerate Directory Entry Settings
One of the nice things about the 2.0 classes is the ability to get and set a configuration object for your directoryEntry objects.
static void DirectoryEntryConfigurationSettings(string domainADsPath)
{
// Bind to current domain
DirectoryEntry entry = new DirectoryEntry(domainADsPath);
DirectoryEntryConfiguration entryConfiguration = entry.Options;
Console.WriteLine(“Server: ” + entryConfiguration.GetCurrentServerName());
Console.WriteLine(“Page Size: ” + entryConfiguration.PageSize.ToString());
Console.WriteLine(“Password Encoding: ” + entryConfiguration.PasswordEncoding.ToString());
Console.WriteLine(“Password Port: ” + entryConfiguration.PasswordPort.ToString());
Console.WriteLine(“Referral: ” + entryConfiguration.Referral.ToString());
Console.WriteLine(“Security Masks: ” + entryConfiguration.SecurityMasks.ToString());
Console.WriteLine(“Is Mutually Authenticated: ” + entryConfiguration.IsMutuallyAuthenticated().ToString());
Console.WriteLine();
Console.ReadLine();
}
Active Directory Objects:
//These methods require these imports
//You must add a references in your project as well
using System.DirectoryServices;
Check for the Existence of an Object
This method does not need you to know the distinguishedName, you can concat strings or even guess a location and it will still run (and return false if not found).
public static bool Exists(string objectPath)
{
bool found = false;
if (DirectoryEntry.Exists(“LDAP://” + objectPath))
{
found = true;
}
return found;
}
Move an Object From One Location to Another
It should be noted that the string newLocation should NOT include the CN= value of the object. The method will pull that from the objectLocation string for you. So object CN=group,OU=GROUPS,DC=contoso,DC=com is sent in as the objectLocation but the newLocation is something like: OU=NewOUParent,DC=contoso,DC=com. The method will take care of the CN=group.
public void Move(string objectLocation, string newLocation)
{
//For brevity, removed existence checks
DirectoryEntry eLocation = new DirectoryEntry(“LDAP://” + objectLocation);
DirectoryEntry nLocation = new DirectoryEntry(“LDAP://” + newLocation);
string newName = eLocation.Name;
eLocation.MoveTo(nLocation, newName);
nLocation.Close();
eLocation.Close();
}
Enumerate Multi-String Attribute Values of an Object
This method includes a recursive flag in case you want to recursively dig up properties of properites such as enumerating all the member values of a group and then getting each member group’s groups all the way up the tree. Collapse
public ArrayList AttributeValuesMultiString(string attributeName,
string objectDn, ArrayList valuesCollection, bool recursive)
{
DirectoryEntry ent = new DirectoryEntry(objectDn);
PropertyValueCollection ValueCollection = ent.Properties[attributeName];
IEnumerator en = ValueCollection.GetEnumerator();
while (en.MoveNext())
{
if (en.Current != null)
{
if (!valuesCollection.Contains(en.Current.ToString()))
{
valuesCollection.Add(en.Current.ToString());
if (recursive)
{
AttributeValuesMultiString(attributeName, “LDAP://” + en.Current.ToString(), valuesCollection, true);
}
}
}
}
ent.Close();
ent.Dispose();
return valuesCollection;
}
Enumerate Single String Attribute Values of an Object
public string AttributeValuesSingleString(string attributeName, string objectDn)
{
string strValue;
DirectoryEntry ent = new DirectoryEntry(objectDn);
strValue = ent.Properties[attributeName].Value.ToString();
ent.Close();
ent.Dispose();
return strValue;
}
Enumerate an Object’s Properties (The Ones With Values)
public static ArrayList GetUsedAttributes(string objectDn)
{
DirectoryEntry objRootDSE = new DirectoryEntry(“LDAP://” + objectDn);
ArrayList props = new ArrayList();
foreach (string strAttrName in objRootDSE.Properties.PropertyNames)
{
props.Add(strAttrName);
}
return props;
}
Get an Object DistinguishedName (ADO.NET Search) – ADVANCED
This method is the glue that ties all the methods together since most all the methods require the consumer to provide a distinguishedName.
Where ever you put this code, you must ensure that you also add these enumerations as well. This allows the consumer to specifiy the type of object to search for and whether they want the distinguishedName returned or the objectGUID.
public enum objectClass
{
user,
group,
computer
}
public enum returnType
{
distinguishedName,
ObjectGUID
}
A call to this class might look like:
myObjectReference.GetObjectDistinguishedName(objectClass.user, returnType.ObjectGUID, “john.q.public”, “contoso.com”)
public string GetObjectDistinguishedName(objectClass objectCls, returnType, returnValue, string ObjectName, string LdapDomain)
{
string distinguishedName = string.Empty;
string connectionPrefix = “LDAP://” + LdapDomain;
DirectoryEntry entry = new DirectoryEntry(connectionPrefix);
DirectorySearcher mySearcher = new DirectorySearcher(entry);
switch (objectCls)
{
case objectClass.user:
mySearcher.Filter = “(&(objectClass=user)(|(cn=” + objectName + “)(sAMAccountName=” + objectName + “)))”;
break;
case objectClass.group:
mySearcher.Filter = “(&(objectClass=group)(|(cn=” + objectName + “)(dn=” + objectName + “)))”;
break;
case objectClass.computer:
mySearcher.Filter = “(&(objectClass=computer)(|(cn=” + objectName + “)(dn=” + objectName + “)))”;
break;
}
SearchResult result = mySearcher.FindOne();
if (result == null)
{
throw new NullReferenceException(“unable to locate the distinguishedName for the object ” +
objectName + ” in the ” + LdapDomain + ” domain”);
}
DirectoryEntry directoryObject = result.GetDirectoryEntry();
if (returnValue.Equals(returnType.distinguishedName))
{
distinguishedName = “LDAP://” + directoryObject.Properties["distinguishedName"].Value;
}
if (returnValue.Equals(returnType.ObjectGUID))
{
distinguishedName = directoryObject.Guid.ToString();
}
entry.Close();
entry.Dispose();
mySearcher.Dispose();
return distinguishedName;
}
Convert distinguishedName to ObjectGUID
public string ConvertDNtoGUID(string objectDN)
{
//Removed logic to check existence first
DirectoryEntry directoryObject = new DirectoryEntry(objectDN);
return directoryObject.Guid.ToString();
}
Convert an ObjectGUID to OctectString (the native ObjectGUID)
public static string ConvertGuidToOctectString(string objectGuid)
{
System.Guid guid = new Guid(objectGuid);
byte[] byteGuid = guid.ToByteArray();
string queryGuid = “”;
foreach (byte b in byteGuid)
{
queryGuid += @”\” + b.ToString(“x2″);
}
return queryGuid;
}
Search By ObjectGUID or Convert ObjectGUID to distinguishedName
public static string ConvertGuidToDn(string GUID)
{
DirectoryEntry ent = new DirectoryEntry();
String ADGuid = ent.NativeGuid;
DirectoryEntry x = new DirectoryEntry(“LDAP://{GUID=” + ADGuid + “>”); //change the { to
return x.Path.Remove(0,7); //remove the LDAP prefix from the path
}
Publish Network Shares in Active Directory
//Example
private void init()
{
CreateShareEntry(“OU=HOME,dc=baileysoft,dc=com”,
“Music”,
@”\\192.168.2.1\Music”,
“mp3 Server Share”);
Console.ReadLine();
}
//Actual Method
public void CreateShareEntry(string ldapPath,
string shareName,
string shareUncPath,
string shareDescription)
{
string oGUID = string.Empty;
string connectionPrefix = “LDAP://” + ldapPath;
DirectoryEntry directoryObject = new DirectoryEntry(connectionPrefix);
DirectoryEntry networkShare = directoryObject.Children.Add(“CN=” + shareName, “volume”);
networkShare.Properties["uNCName"].Value = shareUncPath;
networkShare.Properties["Description"].Value = shareDescription;
networkShare.CommitChanges();
directoryObject.Close();
networkShare.Close();
}
Create a New Security Group
Note: by default if no GroupType property is set, the group is created as a domain security group
public void Create(string ouPath, string name)
{
if (!DirectoryEntry.Exists(“LDAP://CN=” + name + “,” + ouPath))
{
try
{
DirectoryEntry entry = new DirectoryEntry(“LDAP://” + ouPath);
DirectoryEntry group = entry.Children.Add(“CN=” + name, “group”);
group.Properties["sAmAccountName"].Value = name;
group.CommitChanges();
}
catch (Exception e)
{ Console.WriteLine(e.Message.ToString()); }
}
else { Console.WriteLine(path + ” already exists”); }
}
Delete a Group
public void Delete(string ouPath, string groupPath)
{
if (DirectoryEntry.Exists(“LDAP://” + groupPath))
{
try
{
DirectoryEntry entry = new DirectoryEntry(“LDAP://” + ouPath);
DirectoryEntry group = new DirectoryEntry(“LDAP://” + groupPath);
entry.Children.Remove(group);
group.CommitChanges();
}
catch (Exception e)
{ Console.WriteLine(e.Message.ToString()); }
}
else { Console.WriteLine(path + ” doesn’t exist”); }
}
Active Directory Users Tasks:
//These methods require these imports
//You must add a references in your project as well
using System.DirectoryServices;
Add User to Group
public void AddToGroup(string userDn, string groupDn)
{
try
{
DirectoryEntry dirEntry = new DirectoryEntry(“LDAP://” + groupDn);
dirEntry.Properties["member"].Add(userDn);
dirEntry.CommitChanges();
dirEntry.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString();
}
}
Remove User From Group
public void RemoveUserFromGroup(string userDn, string groupDn)
{
try
{
DirectoryEntry dirEntry = new DirectoryEntry(“LDAP://” + groupDn);
dirEntry.Properties["member"].Remove(userDn);
dirEntry.CommitChanges();
dirEntry.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString();
}
}
Get User Group Memberships of the Logged In User (From ASP.NET)
public ArrayList Groups()
{
ArrayList groups = new ArrayList();
foreach (System.Security.Principal.IdentityReference group in
System.Web.HttpContext.Current.Request.LogonUserIdentity.Groups)
{
groups.Add(group.Translate(typeof(System.Security.Principal.NTAccount)).ToString());
}
return groups;
}
Get User Group Memberships
This method requires that you have the AttributeValuesMultiString method earlier in the article included in your class.
public ArrayList Groups(string userDn, bool recursive)
{
ArrayList groupMemberships = new ArrayList();
return AttributeValuesMultiString(“memberOf”, userDn,
groupMemberships, recursive);
}
Create User Account
public string CreateUserAccount(string ldapPath, string userName, string userPassword)
{
try
{
string oGUID = string.Empty;
string connectionPrefix = “LDAP://” + ldapPath;
DirectoryEntry dirEntry = new DirectoryEntry(connectionPrefix);
DirectoryEntry newUser = dirEntry.Children.Add(“CN=” + userName, “user”);
newUser.Properties["samAccountName"].Value = userName;
newUser.CommitChanges();
oGUID = newUser.Guid.ToString();newUser.Invoke(“SetPassword”, new object[] { userPassword });
newUser.CommitChanges();
dirEntry.Close();
newUser.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString();
}
return oGUID;
}
Enable a User Account
public void Enable(string userDn)
{
try
{
DirectoryEntry user = new DirectoryEntry(userDn);
int val = (int)user.Properties["userAccountControl"].Value;
user.Properties["userAccountControl"].Value = val & ~0×2; //ADS_UF_NORMAL_ACCOUNT;
user.CommitChanges();
user.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString(); }
}
Disable a User Account
public void Disable(string userDn)
{
try
{
DirectoryEntry user = new DirectoryEntry(userDn);
int val = (int)user.Properties["userAccountControl"].Value;
user.Properties["userAccountControl"].Value = val | 0×2; //ADS_UF_ACCOUNTDISABLE;
user.CommitChanges();
user.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString();
}
}
Unlock a User Account
public void Unlock(string userDn)
{
try
{
DirectoryEntry uEntry = new DirectoryEntry(userDn);
uEntry.Properties["LockOutTime"].Value = 0; //unlock account
uEntry.CommitChanges(); //may not be needed but adding it anyways
uEntry.Close();
}
catch (System.DirectoryServices.DirectoryServicesCOMException E)
{
//doSomething with E.Message.ToString();
}
}
Reset a User password
public void ResetPassword(string userDn, string password)
{
DirectoryEntry uEntry = new DirectoryEntry(userDn);
uEntry.Invoke(“SetPassword”, new object[] { password });
uEntry.Properties["LockOutTime"].Value = 0; //unlock account
uEntry.Close();
}
Rename an Object
public static void Rename(string objectDn, string newName)
{
DirectoryEntry child = new DirectoryEntry(“LDAP://” + objectDn);
child.Rename(“CN=” + newName);
}
Conclusion
I would have liked to include a sample project but my professional code is so tightly integrated with customer proprietary code that is was not feasible at this time.
If you would like to see an extremely simple implementation of some of this code check out the DirectoryServicesBrowserDialog I posted some time ago. This should demonstrate to those of you who are having trouble adding the proper references or other difficulties.
I hope this helps out all those programmers that were like me and had to get up to speed really quickly and lost countless hours studying the System.DirectoryServices assembly trying to dig up answers on how to do AD tasks.
If you have some additional segments of code that are small but efficient that you’d like to include send them to me and I’ll add them to this document.
About this entry
You’re currently reading “CodeProject; Tools & Code: Working With Active Directory In C#,” an entry on TECH NOTES
- Published:
- April 24, 2007 / 2:41 pm
- Category:
- Code Project Picks