At work, I’ve been working on a lot of automation lately and I ran into a seemingly simple problem that ended up being a bit more complicated than I had first imagined. I have been collaborating on a project that we’re using for auditing Active Directory users and groups and tracking changes to those groups via some simple automation. While that project is interesting in its own right, my boss and I agreed that tackling another helpful automation problem would help our entire IT team: determining if user accounts are locked. I’ve been pushing #ChatOps hard at work through Lita, so adding a plugin for our bot to work with Active Directory seemed only logical.
Context out of the way, making Ruby work with LDAP is a solved problem, many times over. Thankfully, Active Directory exposes most everything you’d want via LDAP, so with a few helper methods, building a few objects tailored to this task was easy work. We quickly discovered that each Active Directory user has a handy attribute called lockoutTime
, and even some helpful hints via the interwebs that we just need to check if that value is 0
(meaning the user isn’t locked out) or any other value (indicating, naturally, that they are locked out). Well, this would be a pretty crappy blog post if that was the end, but it wasn’t.
Our basic testing of “Lock yourself out, check if the bot knows you’re locked out, then have someone unlock you, then have the bot check” worked just fine. That said, Active Directory had a gotcha buried deep in there, and it took a couple hours to stumble upon it. A user that is locked out but doesn’t have an admin unlock them will only be locked for so long. This duration — aptly named lockoutDuration
in Active Directory’s LDAP parlance — is available by querying the LDAP Base DN (usually something like DC=yourdomain,DC=com
). Oddly, this is stored as a negative integer, whose value is expressed in units of 1/10th of a microsecond (or, put another way, 100 nanoseconds). To complicate things further, this is returned by Ruby as a BER encoded string (of the Net::BER::BerIdentifiedString
class to be precise). If that isn’t strange enough, when the lockoutTime
attribute isn’t 0
, it is the timestamp of the user lock-out based on Microsoft’s epoch for Active Directory timestamps. Microsoft’s Active Directory epoch is 1601-01-01 at 00:00, and timestamps are measured in this 100 nanosecond unit as well.
So this means we need to:
- Determine a consistent unit of measurement (we’ll call this
t
) - Calculate the difference between Microsoft’s epoch time and everyone else’s (we’ll call this
epoch_delta
) - To do this, we need to calculate how many units of
t
there are between 1601-01-01 and 1970-01-01. Thankfully, Ruby’sTime
provides a way to do this quickly:
1 2 3 4 5 6 7 8 9 10 11 12 |
# Convert to an instance of Ruby's Time class time_at_ms_epoch = Time.utc("1601-01-01 00:00") # Get the number of seconds, but not negative seconds_since_ms_epoch = time_at_ms_epoch.to_i * -1 # Convert this to units of t t_since_ms_epoch = seconds_since_ms_epoch * 10_000_000 # => 116444736000000000 # As a one-liner epoch_delta = Time.utc("1601-01-01 00:00").to_i * -10_000_000 # => 116444736000000000 |
- Determine the value of the user’s
lockoutTime
attribute (we’ll call thislocktime
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
config = { host: 'ad.yourdomain.com', port: 389, base: 'DC=youdomain,DC=com' auth: { method: :simple, username: 'cn=someuser,CN=Users,DC=youdomain,dc=com', password: 'SuchSecretSoSafeWowSecur3!' } client = Net::LDAP.new(config) client.bind myuser = 'joeblow' joe = client.search( filter: "(sAMAccountName=#{myuser})", base: "CN=Users,#{config[:base]}", attributes: ['lockoutTime'], scope: Net::LDAP::SearchScope_BaseObject ).last # Get the data we want and coerce it into an Integer locktime = Integer(joe['lockoutTime'].last.to_s) # => 131232086303578348 |
- Pull the value of the domain’s
lockoutDuration
attribute (let’s call thisduration
)
1 2 3 4 5 6 7 8 9 10 11 |
domain_obj = client.search( filter: "(objectClass=domain)", base: config[:base], attributes: ['lockoutDuration'], scope: Net::LDAP::SearchScope_BaseObject ).last # Get the data we want and coerce it into an Integer duration = Integer(domain['lockoutDuration'].last.to_s) * -1 # => 36000000000 |
- Get the current time in units of
t
(we’ll call thisnow
):
1 2 |
now = Time.now.to_i * 10_000_000 |
With all that information, we can apply the following calculation / logic:
1 2 3 4 5 |
def locked? return false if locktime.zero? now - (locktime - epoch_delta) < duration end |
Here is where you can see this actual formula applied in practice.