BACKGROUND
In a recent project I had to implement a “Get Stale Accounts” script, which would be used for reporting and automatic user disabling. An account is deemed “stale” if it has not logged in for 6 months. At a first glance, this seems like a simple task – run LDAP query, i.e. (lastLogonTimeStamp<180 days), but as you will see, is quite a bit trickier. In this implementation I used Tools4ever UMRA and we’re currently testing this in UAT (seems to be working good).
lastLogon vs. lastLogonTimeStamp
As you may or may not know, lastLogon AD attribute has been around since the NT days and has caused developers like myself much grief. Reason is it is not replicated between the domain controllers (“DCs”). This means to truly know the last logon time, you would need to query every single DC and calculate the most recent value.
In 2003 AD lastLogonTimeStamp was introduced – same as lastLogon, except replicated. Great news right? Not so quick. By default, this attribute is replicated with a 15 day lag. Read more here. While this replication can be configured in the domain settings, it was not a luxury of my implementation, so I had to stick with lastLogon.
I should point out that both attributes are stored as count of “100s of nano seconds since 1601”.
createTimeStamp vs. whenCreated
It is possible for an account to to be stale, but also to have never logged on. In such case, lastLogon cannot be used as it would be set to 0 on all DCs. Instead, we can ask “if this account never logged on, has it existed for 180 days to be stale?” createTimeStamp AD attribute might jump to mind, but it’s no good because it’s a constructed attribute and cannot be used in an LDAP filter.
Instead, we’ll be using whenCreated. What’s interesting about this attribute is that the underlying AD value is not stored in the standard AD “100s of nano seconds since 1601” format. Instead, it’s stored in format of: “YYYYMMDDHHMMSS.0Z”. Let’s say whenCreated is 10/11/2011; the following filters would result in:
- (whenCreated>=20111010010101.0Z) – MATCH
- (whenCreated>=20111011010101.0Z) – MATCH
- (whenCreated>=20111012010101.0Z) – NO MATCH
FIRST DESIGN
Here’s the first pseudo code I came up with:
- %TimeLimit% = today – 180 days
- Get all accounts that existed long enough to possibly be stale
- Query just 1 DC for (createdTimeStamp <= %TimeLimit%)
- This is %StaleAccountsTable%
- Loop on %StaleAccountsTable%; for every account:
- Loop on every DC; for every DC:
- Query DC for lastLogon (for the current user) & store newest value in the %StaleAccountsTable%
- Perform “is stale” check in code against the freshest lastLogon
- Remove user from table if not stale
- Return %StaleAccountsTable%
…while this should work, I didn’t like the performance: AD would be queried (Number of DCs * Number of Accounts +1) times. If we’re talking about 5 DCs and 50,000 accounts (my project), that’s 250,000+ AD calls.
FINAL DESIGN
The following is the design I ended up implementing. Instead of 250,000 AD calls, I’m now looking at 5. Quite the improvement, eh?
- %TimeLimit% = today – 180 days
- Create %StaleAccountsTable% with columns: SID, lastLogon, any additional data
- Loop on every DC
- Query DC for all accounts that existed long enough to possibly be stale
- LDAP Filter: (createdTimeStamp <= %TimeLimit%)
- Table var: %StaleAccountsTable_Temp%, same columns as %StaleAccountsTable%
- If %StaleAccountsTable% is empty – simply copy %StaleAccountsTable_Temp% into it and resume DC loop, otherwise…
- Loop on %StaleAccountsTable_Temp%
- Search %StaleAccountsTable% for matching SID
- If not found – copy the row over to %StaleAccountsTable% and resume _Temp loop; otherwise…
- Update lastLogon stamp inside %StaleAccountsTable%, only if it’s newer
- Loop on %StaleAccountsTable% and remove any rows where lastLogon does not meet the %TimeLimit% criteria
- Return %StaleAccountsTable%
CONCLUSION
When I set out to implement this script I thought I’d be able to whip it out in a few hours, especially having UMRA at my disposal. After much “wrestling” with AD attribute inconsistencies, I was able to finish this after almost 2 days of work. Let me know if you’re interested in the final UMRA script; I’ll see if I can throw it up here.