Article C0007 C# .NET
Talking LDAP

For one of my recent projects I had to talk with the Microsoft Windows 2003 Active Directory Services. This article shows the attributes I needed and explains some of the invocation basics and exception management.

When you are going to write an application which is going to talk with the Active Directory you’ll have to include the Directory Services namespace:

    using System.DirectoryServices;
Depending on the tasks you are going to fulfill with your application, you might also need to add a reference towards the ActiveDS.dll as well. This DLL allows access to the Active Directory Services Interface, ADSI. It is simply not possible to change all attributes using plain LDAP calls. You can add a reference by fulfilling the following steps:
For almost all activities towards the Active Directory the distinguished name of the target object is required. To obtain the distinguished name of an object you can use the following routine which can be found at ‘The Code Project’ website:

http://www.codeproject.com/KB/system/everythingInAD.aspx

    public enum objectClass
    {
        user, group, computer
    }
    public enum returnType
    {
        distinguishedName, ObjectGUID
    }
    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)
        {
            rhrow 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;
}
An important point of attention is the way you address your questions towards the Active Directory. In the shown procedure the connection is made using DirectoryEntry using the following convention:

	DirectoryEntry("LDAP://<domainname>");
If the application is used from within the domain the common name can be used, for example, the TESTDOMAIN.EDU can be queried like this:

	DirectoryEntry("LDAP://TESTDOMAIN");
Bear in mind that the string 'LDAP://' is case sensitive, the query will fail when written incorrectly. Now, let’s have a look at the following function which is using ADSI, reset a password:

    // Try to reset the password:
    try
    {
        string user_dn = mainForm.GetObjectDistinguishedName(objectClass.user,
        	returnType.distinguishedName, <selectedUser>, <withinDomain>);
        
        DirectoryEntry userEntry = new DirectoryEntry(userDn);
        userEntry.Invoke("SetPassword", new object[] { password });
        userEntry.Properties["LockOutTime"].Value = 0;  // Unlock account
        userEntry.Properties["pwdLastSet"].Value = 0;   // Change password @ next logon
        userEntry.CommitChanges();
        userEntry.Close();
        userEntry.Dispose();
    }
    catch (UnauthorizedAccessException)
        {
        MessageBox.Show("Insufficient Rights", "Error",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    catch (Exception err)
        {
        if (err.InnerException.GetType() == typeof(UnauthorizedAccessException))
        	{
	        MessageBox.Show("Insufficient Rights","Error!", MessageBoxButtons.OK,
        	MessageBoxIcon.Error);
	    }
        else
        	MessageBox.Show("Error: "+err.Message, Mess.Err,
    MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
What you can see here is that the actual password reset takes place using ADSI and that the both lockOutTime and pwdLastSet are changed straight through LDAP. Lots of things can go wrong during this process, so we have to handle all sorts of exceptions when calling this code. So the code after the 'try' is caught on several exceptions.

The first 'UnauthorizedAccessException' exception is required for the lockOutTime and the pwdLastSet items. This exception might be raised when we have no right to fulfill this task. In normal circumstances we are not allowed to fulfill this task and a high privileged principal like a Domain Administrator need to provide Delegation of Control to the application user(s).

The second ‘Exception’ exception is needed to catch other exceptions which we cannot directly see. This due to the fact that we have asked ADSI to do some work for us, so exceptions out of ADSI will be wrapped.

The third 'InnerException' is read out of the previous exception using the GetType() method:

	err. InnerException.GetType()
In our code we check the type of exception, which could also be a ‘UnauthorizedAccessException’ exception.

Finally we just show the exception so that we can see the exception type and enhance the ‘try..catch’ routine even more.

So now you have seen the basics of accessing the Active Directory, the next item is on how to access, change or clear an attribute. The following code is an example on how to create a new account:

    string prefix = "LDAP://" + <FQDN of base OU>
    DirectoryEntry dirEntry = new DirectoryEntry(prefix);
    DirectoryEntry newUser = dirEntry.Children.Add("CN=" + edtLogon.Text, "user");
    try
    {
	    newUser.Properties["samAccountName"].Value = edtLogon.Text;
	    newUser.CommitChanges();
    }
    catch (UnauthorizedAccessException)
    {
	    MessageBox.Show("Insufficient Rights", "Error!", MessageBoxButtons.OK,
	    MessageBoxIcon.Error);
	    return;
    }
    catch(Exception)
    {
	    MessageBox.Show("Account Already Exists", "Error!", MessageBoxButtons.OK,
	    MessageBoxIcon.Error);
	    return;
    }
    newUser.Properties["userPrincipalName"].Value = edtLogonName.Text +
	    <domain suffix>;
    if (edtFName.Text.Length > 0) newUser.Properties["givenName"].Value = edtFName.Text;
    if (edtInits.Text.Length > 0) newUser.Properties["initials"].Value = edtInits.Text;
    if (edtLName.Text.Length > 0) newUser.Properties["sn"].Value = edtLName.Text;
    if (edtDisp.Text.Length > 0) newUser.Properties["displayName"].Value = edtDisp.Text;
    newUser.CommitChanges();
    
    newUser.Invoke("SetPassword", new object[] { edtPW.Text });
    newUser.Properties["pwdLastSet"].Value = 0; // Change password @ next logon
    newUser.CommitChanges();
    dirEntry.Close();
    dirEntry.Dispose();
    newUser.Close();
    newUser.Dispose();
To create a new account the Full Qualified Domain Name of the Organizational Unit must be provided. Within this OU the account is created, this way you can avoid creation of accounts within the default ‘Users’ container.

As shown within the function you’ll have to check if the attribute value really contains data. If not, the code will fail with an exception and the user creation process will not be complete. One tricky thing to avoid is the fact that the length of the Initials attribute has a maximum length of six, so be sure to set the TextBox maximum length to six.

When you want to get rid of the content of an attribute simple pass a null value into it:

	userEntry. Properties["givenName"].Value = null;
	userEntry. CommitChanges();
Finally, a table of common used Active Directory attributes is available here.

~Edward