Introduction
For the sake of enhanced security at my company, I was asked to develop a way to determine the last time all of the company employees logged into our network. This isn't as easy as you might think. There are generally multiple servers in the domain and the lastLogon value for any one individual user could be different on each of those servers. I was familiar with the System.DirectoryServices namespace, but there were other issues, as you will see.
Background
One of the network analysts at my company gave me a VBScript he had found that loads a Dictionary object with this exact information. Richard L. Mueller of Hilltop Lab generously provides this script free-of-charge on his website. See the Acknowledgements section below for links to the script itself. It seemed that doing the same thing in C# using the .NET Framework should be a trivial task. Much did, in fact, convert easily from VBScript to C#. However, there were a few missing pieces that took me a good deal of time to figure out. Hence, this article.
The root of the matter
The first step is to locate the root DirectoryEntry
for your domain.
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://RootDSE");
string configurationNamingContext =
(string)rootDSE.Properties["configurationNamingContext"].Value;
string defaultNamingContext =
(string)rootDSE.Properties["defaultNamingContext"].Value;
The RootDSE
path gets the root entry for the whole domain. Here, I must capture two values: configurationNamingContext
and defaultNamingContext
. The first value will be used immediately in the next step and the second will be required further along.
Master of every domain [server]
Using configurationNamingContext
, you can now process all of the users from each of the domain controllers in your domain.
DirectoryEntry deConfig =
new DirectoryEntry("LDAP://" + configurationNamingContext);
DirectorySearcher dsConfig = new DirectorySearcher(deConfig);
dsConfig.Filter = "(objectClass=nTDSDSA)";
foreach (SearchResult srDomains in dsConfig.FindAll())
{
DirectoryEntry deDomain = srDomains.GetDirectoryEntry();
if (deDomain != null)
{
string dnsHostName =
deDomain.Parent.Properties["DNSHostName"].Value.ToString();
}
}
Let's go through the key issues here. First, you can see that you use configurationNamingContext
, captured in the root entry in the previous step, as your LDAP path. To find the domain controllers, you need to filter using (objectClass=nTDSDSA)
. Then, for each domain controller you find, you need to look for the DNSHostName
of its Parent
entry. Once you get to this point, you're ready to get the lastLogon
s for all of the users on that domain controller.
Users are persons
Yes, I know, you may not always think so. But according to the Active Directory, all users are persons. At least, that's how you have to search for them.
DirectoryEntry deUsers =
new DirectoryEntry("LDAP://" + dnsHostName + "/" + defaultNamingContext);
DirectorySearcher dsUsers = new DirectorySearcher(deUsers);
dsUsers.Filter = "(&(objectCategory=person)(objectClass=user))";
foreach (SearchResult srUsers in dsUsers.FindAll())
{
DirectoryEntry deUser = srUsers.GetDirectoryEntry();
if (deUser != null)
{
}
}
Now we are finally getting down to the nitty-gritty. Notice that the path here includes two previously captured pieces of information: DNSHostName
and defaultNamingContext
. This last one was been captured way back at the root entry. (See, I told you we would eventually need it.) We are now going to capture two pieces of information for each user: their distinguishedName
and their lastLogon
. Once we have those, we'll add them to the Dictionary
object. Before we get to that, though, I must backtrack a bit and talk about references and the Dictionary
object.
References and the dictionary object
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using ActiveDs;
First, the references. If you are going to convert lastLogon
to a readable DateTime
object, you'll need the System
namespace that is included by default in most classes anyway. The System.Collections.Generic
namespace contains the Dictionary
class, which will be used in the next step to capture the distinguishedName
and lastLogon
data for each user. The distinguishedName
value is used because it uniquely identifies each user. For example, here's what mine looks like:
CN=Dwight Johnson, OU=Corporate, DC=preitllc, DC=com
lastLogon
is a tricky beast. It is stored as an IADsLargeInteger
object in the active directory. However, to add the reference for that class, you must select the COM tab and look for the Active DS Type library. This shows up in the References list as ActiveDs. Finally, to store the user's logon information, declare a Dictionary
object. Since it is a generic class, define it for a string to hold distinguishedName (key)
and for Int64
to hold lastLogon (value)
.
Dictionary<string, Int64> lastLogons = new Dictionary<string, Int64>();
Save the latest of the last
Now it finally becomes clear why the logon information is being stored in a Dictionary
object, which I have named lastLogons
. As you roll through all of the users on each Domain Server, you store distinguishedName
and lastLogon
in the Dictionary. On the first domain controller, you will just be putting each distiguishedName
and lastLogon
into a new key / value pair in the Dictionary. However, when processing all of the users on the next and subsequent controllers, you will likely run into distinguishedName
s that you've already stored. In those cases, you want to compare the lastLogon
previously stored -- i.e. lastLogons[distinguishedName]
, where distinguishedName
is the key and the lastLogon
is the value -- with the lastLogon
of the current server, lastLogonThisServer
. The most recent one -- i.e. the latest of the last -- will be saved.
string distinguishedName =
deUser.Properties["distinguishedName"].Value.ToString();
Int64 lastLogonThisServer = new Int64();
if (deUser.Properties["lastLogon"].Value != null)
{
IADsLargeInteger lgInt =
(IADsLargeInteger)deUser.Properties["lastLogon"].Value;
lastLogonThisServer = ((long)lgInt.HighPart << 32) + lgInt.LowPart;
}
if (lastLogons.ContainsKey(distinguishedName))
{
if (lastLogons[distinguishedName] < lastLogonThisServer)
{
lastLogons[distinguishedName] = lastLogonThisServer;
}
}
else
{
lastLogons.Add(distinguishedName, lastLogonThisServer);
}
By the way, please note the tricky little calculation that is required to convert the date and time information from IADsLargeInteger
into Int64
. The date is stored in the HighPart
and the time in the LowPart
.
What time is it?
Finally, if you want to see lastLogon
as a DateTime
value, there is a method in the System
namespace that will do this.
string readableLastLogon =
DateTime.FromFileTime(lastLogonThisServer).ToString();
Acknowledgments
I would like to thank Yann LeGuen, who brought the original VBScript to my attention. I would also like to again thank Richard L. Mueller for the excellent script that provided so much useful information.
Conclusion
The Active Directory is a vital part of Windows networks. I hope this article makes dealing with it a bit easier.
History
- 14 June, 2007 -- Original version posted