Home > Articles > Programming > Windows Programming

This chapter is from the book

Managing User Account Features

Creating user objects in Active Directory is not difficult. As we have seen, with the right permissions, we can create a basic user account in just a few lines of code.

However, getting user objects to behave like Windows user accounts is a bit more challenging. Windows accounts have many features, such as enabled/disabled status, names and identifications used for security and email, password management, and expiration and lockout status, all of which require more-intimate knowledge of how things work under the hood. The next several sections explore this in detail.

Managing Basic User Account Properties in Active Directory

Many of the important behaviors associated with a Windows account in Active Directory, such as enabled/disabled status, are controlled by an attribute called userAccountControl. This attribute contains a 32-bit integer that represents a bitwise enumeration of various flags that control account behavior.

These flags are represented in ADSI by an enumerated constant called ADS_USER_FLAG. Because this enumeration is so important in terms of working with user objects in System.DirectoryServices (SDS), we will convert the ADSI enumeration into a .NET-style enumeration, as shown in Listing 10.2.

Listing 10.2: User Account Control Flags

[Flags]
public enum AdsUserFlags
{ 
  Script = 1,                  // 0x1
  AccountDisabled = 2,              // 0x2
  HomeDirectoryRequired = 8,           // 0x8 
  AccountLockedOut = 16,             // 0x10
  PasswordNotRequired = 32,           // 0x20
  PasswordCannotChange = 64,           // 0x40
  EncryptedTextPasswordAllowed = 128,      // 0x80
  TempDuplicateAccount = 256,          // 0x100
  NormalAccount = 512,              // 0x200
  InterDomainTrustAccount = 2048,        // 0x800
  WorkstationTrustAccount = 4096,        // 0x1000
  ServerTrustAccount = 8192,           // 0x2000
  PasswordDoesNotExpire = 65536,         // 0x10000
  MnsLogonAccount = 131072,           // 0x20000
  SmartCardRequired = 262144,          // 0x40000
  TrustedForDelegation = 524288,         // 0x80000
  AccountNotDelegated = 1048576,         // 0x100000
  UseDesKeyOnly= 2097152,            // 0x200000
  DontRequirePreauth= 4194304,          // 0x400000
  PasswordExpired = 8388608,           // 0x800000
  TrustedToAuthenticateForDelegation = 16777216, // 0x1000000
  NoAuthDataRequired = 33554432         // 0x2000000
}

As we look through the members of this enumeration, we see a variety of words we associate with Windows accounts, such as AccountDisabled and PasswordNotRequired (the last one we hope you never use!). We also see some flags that we probably do not recognize, such as MnsLogonAccount and UseDesKeyOnly. For the most part, the esoteric flags are not important in daily account management tasks, so we can ignore them. Chances are, if we need these flags we are probably quite aware of them already.

The important thing to note is that even though 21 flags are currently defined for use with the userAccountControl attribute, Active Directory does not actually use all of them! Specifically, the ones that are not meaningful to Active Directory are

  • AccountLockedOut
  • PasswordCannotChange
  • PasswordExpired

Active Directory actually uses different mechanisms to control these account properties, so do not try to read them from userAccountControl! We discuss how to deal with the special cases in the upcoming sections.

Reading User Account Properties

Reading the userAccountControl attribute is simple. Listing 10.3 uses an enumeration we defined in Listing 10.2.

Listing 10.3: Reading the userAccountControl Attribute

DirectoryEntry user = new DirectoryEntry(
  "LDAP://CN=User1,CN=users,DC=domain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );

AdsUserFlags userFlags = (AdsUserFlags)
  user.Properties["userAccountControl"].Value;

Console.WriteLine(
  "AdsUserFlags for {0}: {1}",
  user.Path,
  userFlags
  );

This will generally write NormalAccount to the console for a typical user and may include other flags as well, depending on our specific deployment. If the account is disabled, AccountDisabled will be displayed in addition.

For Windows Server 2003 Active Directory, we could also use the msDS-User-Account-Control-Computed attribute in place of userAccountControl. Since it is constructed, we would need to use our RefreshCache technique from Chapter 3. However, the one benefit of using this attribute is that the three flags we previously mentioned as not being used with userAccountControl would actually be used and be accurate. This is not an option with Windows 2000 Server Active Directory installations—they will always need to use userAccountControl for reading user account properties.

Writing User Account Properties

Writing values is equally as easy as reading them. We just create an integer value representing the proper combination of flags and overwrite the existing userAccountControl value, as shown in Listing 10.4.

Listing 10.4: Writing Account Values

DirectoryEntry entry = new DirectoryEntry(
  "LDAP://CN=some user,CN=users,DC=mydomain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );

AdsUserFlags newValue = AdsUserFlags.NormalAccount
  | AdsUserFlags.DontExpirePassword;

entry.Properties["userAccountControl"].Value = newValue;
entry.CommitChanges()

The trick here is that we must use valid combinations of flag values. In addition, other aspects of the account or the policies in effect may prevent certain values from being set.

A classic example that can trip up new developers happens when enabling an account by "unsetting" the AccountDisabled flag. In many domains, a minimum password length is required for all user accounts (and hopefully this is what you use as well). An account cannot be enabled unless it has a password. Therefore, we must set a valid password before enabling the account.

As a result, many typical provisioning processes that create accounts follow this protocol.

  1. Create the account with initial values and commit changes.
  2. Use the SetPassword operation to set an initial password.
  3. Enable the account and commit changes again.

Keep this in mind when creating and provisioning user accounts if errors occur.

Delegation Settings

Three flags in the enumeration relate to delegation, which we discussed in Chapter 8. Specifically, TrustedForDelegation and TrustedToAuthenticateForDelegation are used by service accounts that will be allowed to delegate users’ credentials to other machines. The difference between them is that TrustedForDelegation is the "unconstrained" delegation setting that works with Windows 2000 Server, and it represents the flag used when only Kerberos authentication is allowed in constrained delegation. TrustedToAuthenticateForDelegation is new with Windows Server 2003 and is used when delegation from any protocol is allowed. This is known as the "Protocol Transition" setting.

Finally, AccountNotDelegated is used to flag an account as "sensitive and cannot be delegated." This is typically used on highly privileged accounts such as those used by directory administrators, where we would probably not want their account to be delegated by another service due to the security risk it poses.

Managing Basic User Account Properties in ADAM

ADAM works differently than Active Directory in that it does not rely on the userAccountControl attribute to maintain important account properties. Instead, Microsoft introduced a number of attributes prefixed with ms-DS- or msDS to hold this information:

  • ms-DS-UserAccountAutoLocked*
  • msDS-User-Account-Control-Computed*
  • msDS-UserAccountDisabled
  • msDS-UserDontExpirePassword
  • ms-DS-UserEncryptedTextPasswordAllowed
  • msDS-UserPasswordExpired*
  • ms-DS-UserPasswordNotRequired

The * in the preceding list indicates that the attribute is constructed.

With the exception of the integer-valued msDS-User-Account-Control-Computed, these attributes are Boolean values in the directory. Some of these attributes are also constructed attributes and as such, they cannot be written. We wish we could say there was some method behind the slightly different ldapDisplayName prefix values, but it just appears that it was overlooked.

Reading User Account Properties

The constructed attribute called msDS-User-Account-Control-Computed takes the place of userAccountControl in ADAM. This attribute is new to Windows Server 2003 Active Directory and ADAM and we can use it rather than userAccountControl to read account properties. However, given that this is a constructed attribute, we can neither search on any values held within it nor set any values on this attribute. Listing 10.5 demonstrates how similar it is to read this attribute compared to using the userAccountControl attribute in Listing 10.3.

Listing 10.5: Reading the msDS-User-Account-Control-Computed Attribute

DirectoryEntry user = new DirectoryEntry(
  "LDAP://CN=User1,CN=users,DC=domain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );

//this is a pain to type a lot :)
string msDS = "msDS-User-Account-Control-Computed";

using (user)
{
  //this is constructed attribute
  user.RefreshCache(
    new string[]{msDS}
    ); 

  AdsUserFlags userFlags = 
    (AdsUserFlags)user.Properties[msDS].Value;

  Console.WriteLine(
    "AdsUserFlags for {0}: {1}",
    user.Path,
    userFlags
    );
}

We should note that the msDS-User-Account-Control-Computed attribute will accurately hold values like AccountLockedOut, PasswordCannotChange, and PasswordExpired. This departs from the userAccountControl attribute, where these values are not represented accurately because the flags are not used. We also have the option of using the special "alias" attributes such as ms-DS-UserAccountAutoLocked here. We simply read the Boolean value they return. In many cases, this may be more straightforward.

Writing User Account Properties

Since the userAccountControl attribute is not used with ADAM, and its equivalent but constructed attribute cannot be written, we need to use the other msDS and ms-DS attributes to actually set values. Listing 10.6 shows one such example where we can enable or disable an ADAM account.

Listing 10.6: Writing Account Values

string adsPath = 
  "LDAP://localhost:389/"
  + "CN=User1,OU=Users,O=dunnry,C=US";

DirectoryEntry user = new DirectoryEntry(
  adsPath,
  null,
  null,
  AuthenticationTypes.Secure
  );

string attrib = "msDS-UserAccountDisabled";

using (user)
{
  //disable the account
  user.Properties[attrib].Value = true;
  user.CommitChanges();
}

As writing each of the other nonconstructed Boolean account properties is exactly the same, we will not demonstrate further examples. The key point to take away here is that we need to look to these attributes in lieu of using the userAccountControl attribute for ADAM.

Determining Domain-Wide Account Policies

When working with user accounts in Active Directory, it is common to need to refer to domain-wide account policies. For example, policies such as the minimum and maximum password age and the minimum password length, as well as lockout policy, are determined at the domain level and apply to each user object in the domain.

All of the values are stored directly in the domain root object (not in RootDSE, but in the object pointed to by the defaultNamingContext attribute in RootDSE) as a set of attributes such as maxPwdAge, minPwdLength, and lockoutThreshold. Additionally, the password complexity rules are encoded in an enumerated value in the pwdProperties attribute.

These values tend to be quite static in most domains, so we would typically want to read these values only once per program execution. To make the policy values easy to consume, we show in Listing 10.7 a wrapper class for the domain account policies that converts all of the values into convenient .NET data types, such as TimeSpan. A special .NET enumeration type for the types of the password policy is also included. We won’t be able to include all of the class properties in the book, as that would take too much space, but we will have the full class available on the book’s web site.

We will refer to this sample in future discussions when demonstrating how to determine an account’s lockout status and for finding accounts with expiring passwords. It is also worthy to note that any LargeInteger values in these policy attributes are stored as negative values. We chose to invert them back to positive values because it is easier to think about them in this way. Developers choosing to use these attributes should keep this in mind, as it will throw off calculations later if not accounted for.

Listing 10.7: Determining Domain Policies

[Flags]
public enum PasswordPolicy
{
  DOMAIN_PASSWORD_COMPLEX=1,
  DOMAIN_PASSWORD_NO_ANON_CHANGE=2, 
  DOMAIN_PASSWORD_NO_CLEAR_CHANGE=4, 
  DOMAIN_LOCKOUT_ADMINS=8,
  DOMAIN_PASSWORD_STORE_CLEARTEXT=16,
  DOMAIN_REFUSE_PASSWORD_CHANGE=32
}

public class DomainPolicy
{
  ResultPropertyCollection attribs;

  public DomainPolicy(DirectoryEntry domainRoot)
  {
    string[] policyAttributes = new string[] {
      "maxPwdAge", "minPwdAge", "minPwdLength", 
      "lockoutDuration", "lockOutObservationWindow", 
      "lockoutThreshold", "pwdProperties", 
      "pwdHistoryLength", "objectClass", 
      "distinguishedName"
      };

    //we take advantage of the marshaling with
    //DirectorySearcher for LargeInteger values...
    DirectorySearcher ds = new DirectorySearcher(
      domainRoot,
      "(objectClass=domainDNS)",
      policyAttributes,
      SearchScope.Base
      );

    SearchResult result = ds.FindOne();

    //do some quick validation...							  
    if (result == null)
    {
      throw new ArgumentException(
        "domainRoot is not a domainDNS object."
        );
    }

    this.attribs = result.Properties;
  }

  //for some odd reason, the intervals are all stored
  //as negative numbers. We use this to "invert" them
  private long GetAbsValue(object longInt)
  {
    return Math.Abs((long)longInt);
  }

  public TimeSpan MaxPasswordAge
  {
    get
    {
      string val = "maxPwdAge";
      if (this.attribs.Contains(val))
      {
        long ticks = GetAbsValue(
          this.attribs[val][0]
          );

        if (ticks > 0)
          return TimeSpan.FromTicks(ticks);
      }

      return TimeSpan.MaxValue;
    }
  }

  public PasswordPolicy PasswordProperties
  {
    get
    {
      string val = "pwdProperties";
      //this should fail if not found
      return (PasswordPolicy)this.attribs[val][0];
    }
  }
  
  //truncated for book space
}

Listing 10.7 is meant to run on an Active Directory domain. Where does this leave ADAM instances? By default, ADAM will assume any local or domain policies on the Windows 2003 server where it is running. This means that if our Windows 2003 server is a member of the domain, we can simply use code similar to that inListing 10.7. If, however, the server is running in a workgroup configuration, the policy will be determined locally. Therefore, Listing 10.7 would not be appropriate. Instead, we would need to know our local policy or attempt to discover it using Windows Management Instrumentation (WMI) classes.

Determining Password Expiration

Earlier in this chapter, we mentioned that accounts could have passwords that expire. Most Active Directory domains and many ADAM instances force passwords to expire periodically to improve security. As such, we often need to know when a user’s password will expire.

Determining password expiration on user accounts in Active Directory and ADAM might appear tricky, but really, it is a simple matter of calculation. Password expiration is determined based on when an individual password was last changed, and on the domain-wide password expiration policy, which we detailed in the previous section. The algorithm is essentially this:

if "password change date" + "max password age" >= "now"   "password is expired"

Typically, Windows monitors password expiration and will inform a user that her password is expiring soon when she logs on locally to Windows. It then provides a mechanism to change the password. As long as the user changes her password before it expires, she can continue to log in to the domain and all is good. However, if the password expires, then the user cannot log in again until an administrator resets it.

This situation is not as straightforward for ADAM users, as there is no natural "login" process that informs users of pending password expiration and prompts them for a password change. Instead, it is completely up to the developer to supply both a notification and a means by which to change a password when using ADAM.

Programmatic LDAP binds to either directory must be handled explicitly by the developer, as we will not be warned of pending password expiration. Once a password has expired, all LDAP binds will fail until the password is reset by the user or an administrator.

How Password Modification Dates Are Stored

Active Directory and ADAM use the pwdLastSet attribute to record when a password was last changed, via either an end-user password change or an administrative reset. Like most time-based Windows data in the directory, the attribute uses the 2.5.5.16 LargeInteger attribute syntax, which essentially holds a Windows FILETIME structure as an 8-byte integer. We already discussed how to read and write these attributes in Chapter 6, as well as build LDAP search filters based on them in Chapter 4, so that knowledge should be easy to apply to this problem.

There is one edge case that developers should be aware of when dealing with this attribute. Namely, the pwdLastSet attribute can be set to zero (0), which implies that the password is automatically expired and must be changed at next login.

Now that we know the details on how this mechanism works, we are ready to write some code to check this. The first thing we need is a user’s pwdLastSet value as a .NET Int64, or long integer. As per Chapter 6, we can do this using DirectorySearcher and its built-in marshaling of the data, or we can use one of the conversion functions we described for use with DirectoryEntry. For our purposes, we will use DirectorySearcher for converting the LargeInteger values in conjunction with Listing 10.7 to obtain domain policies.

We will step through a larger class we have chosen to name PasswordExpires, explaining as we go the thought process that surrounds what we are trying to accomplish. As such, we might have to refer to previous listings to see any member variables. This is a complete class and it requires a number of lines, but don’t worry about needing to copy it verbatim. We will include it as a sample on the book’s web site under its listing number.

The first part of determining password expiration is to determine our domain policy for the maximum password age (MaxPwdAge). Listing 10.8 shows how we can easily accomplish this using the DomainPolicy class we introduced inListing 10.7 along with some tricks we learned in Chapter 9 using System.DirectoryServices.ActiveDirectory (SDS.AD) classes.

Listing 10.8: PasswordExpires, Part I

public class PasswordExpires
{
  DomainPolicy policy;
  
  const int UF_DONT_EXPIRE_PASSWD = 0x10000;

  public PasswordExpires()
  {
    //get our current domain policy
    Domain domain = Domain.GetCurrentDomain();
    DirectoryEntry root = domain.GetDirectoryEntry();
  
    using (domain)
    using (root)
    {
      this.policy = new DomainPolicy(root);
    }
  }

In Listing 10.8, we are simply using the Domain class from SDS.AD to get a DirectoryEntry object bound to the current domain’s default naming context. We need the root partition of the domain in order to determine our domain policies. At this point, we simply load our DomainPolicy object with our root domainDNS object. Next, we need to calculate the actual DateTime when a user’s password would expire. Listing 10.9 shows how we can accomplish this.

Listing 10.9: PasswordExpires, Part II

  public DateTime GetExpiration(DirectoryEntry user)
  {
    int flags = 
      (int)user.Properties["userAccountControl"][0];

    //check to see if password is set to expire
    if(Convert.ToBoolean(flags & UF_DONT_EXPIRE_PASSWD))
    {
      //the user’s password will never expire
      return DateTime.MaxValue;
    }

    long ticks = GetInt64(user, "pwdLastSet");

    //user must change password at next login
    if (ticks == 0)
      return DateTime.MinValue;

    //password has never been set
    if (ticks == -1)
    {
      throw new InvalidOperationException(
        "User does not have a password"
        );
    }

    //get when the user last set their password;
    DateTime pwdLastSet = DateTime.FromFileTime(
      ticks
      );

    //use our policy class to determine when
    //it will expire
    return pwdLastSet.Add(
      this.policy.MaxPasswordAge
      );
  }

The first thing we do in Listing 10.9 is check to see if the user’s account is set so that the password never expires. We will use the convention that DateTime.MaxValue means it will never expire. Next, we are using a helper function called GetInt64 (see Listing 10.10). This function marshals the user’s pwdLastSet attribute into an Int64 for us using a DirectorySearcher object. Notice that one of three conditions can arise out of this check. First, the pwdLastSet attribute might be null (if it is not set on the object), in which case the user account has no password. We chose to treat the situation where a user does not have a password as an error condition, but this can differ by application. Second, the attribute might be 0, which means that the user must change her password at the next logon. We chose to use the convention that DateTime.MinValue meant that the password must be changed at next log on. Lastly, the attribute might contain some value that we can interpret as a FILETIME structure and can convert using DateTime.FromFileTime. The calculation for determining when a user’s password will expire is simple. We just add the DateTime value of when the user last changed her password to the TimeSpan value of the domain’s MaxPwdAge policy. If the user’s password has already expired, we will still get a DateTime value, but it will be in the past.

Knowing the date a user’s password has expired is nice, but we might actually want to know how much time is left before the user’s password expires. That is a very easy calculation, as Listing 10.10 demonstrates.

Listing 10.10: PasswordExpires, Part III

   public TimeSpan GetTimeLeft(DirectoryEntry user)
  {
    DateTime willExpire = GetExpiration(user);

    if (willExpire == DateTime.MaxValue)
      return TimeSpan.MaxValue;

    if (willExpire == DateTime.MinValue)
      return TimeSpan.MinValue;

    if (willExpire.CompareTo(DateTime.Now) > 0)
    {
      //the password has not expired
      //(pwdLast + MaxPwdAge)- Now = Time Left
      return willExpire.Subtract(DateTime.Now);
    }

    //the password has already expired
    return TimeSpan.MinValue;
  }
  
  private Int64 GetInt64(DirectoryEntry entry, string attr)
  {
    //we will use the marshaling behavior of
    //the searcher
    DirectorySearcher ds = new DirectorySearcher(
      entry,
      String.Format("({0}=*)", attr),
      new string[] { attr },
      SearchScope.Base
      );
      
    SearchResult sr = ds.FindOne();
    
    if (sr != null)
    {
      if (sr.Properties.Contains(attr))
      {
        return (Int64)sr.Properties[attr][0];
      }
    }
    return -1;
  }

We chose to return a TimeSpan value representing the time left before a user’s password would expire in Listing 10.10. If either TimeSpan.MaxValue or TimeSpan.MinValue is returned, it is meant to indicate that the user’s password does not expire or has already expired, respectively. For completeness, we have also included our helper GetInt64 in Listing 10.10, though by now we know that everyone is probably aware of how to marshal LargeInteger values from Chapter 6.

We typically want to do something based on when a password will expire, so it is important to know how much time is left. However, if we just want to know whether an account has expired, we can use the previously mentioned msDS-User-Account-Control-Computed attribute for Windows 2003 Active Directory and ADAM, or the aptly named msDS-UserPasswordExpired attribute for ADAM, to just give us a yes/no answer. Listing 10.11 shows one such example.

Listing 10.11: Checking Password Expiration

string adsPath = "LDAP://CN=User1,OU=Users,DC=domain,DC=com";

DirectoryEntry user = new DirectoryEntry(
  adsPath,
  null,
  null,
  AuthenticationTypes.Secure
  );

string attrib = "msDS-User-Account-Control-Computed";

using (user)
{
  user.RefreshCache(new string[] { attrib });

  int flags = (int)user.Properties[attrib].Value
    & (int)AdsUserFlags.PasswordExpired);

  if (Convert.ToBoolean(flags)
  {
    //password has expired
    Console.WriteLine("Expired");
  }
}

Of course, the problem with something like Listing 10.11 is that we don’t know when the password actually expired or how much time is left before it does expire. Additionally, as we previously mentioned, a solution like this will work with only Windows 2003 Active Directory and ADAM. Windows 2000 Active Directory users must use a solution such as that shown in Listing 10.8.

Searching for Accounts with Expiring Passwords

Another thing we may wish to do is find all of the accounts with passwords expiring within a certain time range, perhaps to send an email notification directing users to a web-based portal where they can change their passwords. This is important for ADAM users and any Active Directory users that do not typically log in to Windows via the workstation.

The crux of this search is based on creating a search filter with the correct values. Let’s say we want to find user accounts with passwords expiring between two dates. Since password expiration is based on the date the password was last changed and the maximum password age domain policy, we subtract the maximum password age from the two dates to get the values of pwdLastSet that will match. The code might look like that shown in Listing 10.12.

Listing 10.12: Finding Expiring Passwords

public static string GetExpirationFilter(
  DateTime startDate,
  DateTime endDate,
  TimeSpan maxPwdAge
  )
{    
  Int64 lowDate;
  Int64 highDate;
  string filterPattern = "(&(sAMAccountType=805306368)" +
    "(pwdLastSet>={0})(pwdLastSet<={1}))"
  
  lowDate = startDate.Subtract(maxPwdAge).ToFileTime();
  highDate = endDate.Subtract(maxPwdAge).ToFileTime();
      
  return String.Format( 
    filterPattern, 
    lowDate, 
    highDate 
    );
}

A complete sample that enumerates users with expiring passwords between two dates is available on this book’s web site.

In both examples, we see that .NET makes this especially easy. The built-in support for dates, time spans, and Windows FILETIME structures simplifies much of the work. We can also easily construct variations on this, using similar techniques, to find accounts whose passwords have already expired, or to find all accounts that will expire before or after a certain date.

Determining Last Logon

The last time a user has logged onto the domain is held in an attribute on the user object. Called lastLogon, this attribute is a nonreplicated attribute, which means that each domain controller holds its own copy of the attribute, likely with different values. Checking the last time a user has logged onto the domain requires us to visit each domain controller and read the attribute. The value found for the latest lastLogon is the value we are after.

We covered in Chapter 9 how to use the Locator to enumerate all the domain controllers. We will use this technique again to iterate through each controller and retrieve the lastLogon attribute for each user. Listing 10.13 demonstrates how to accurately determine the last time a user has logged into the domain.

Listing 10.13: Finding a User’s Last Logon

string username = "user1";
string domain = "mydomain.com";

public static void LastLogon(string username, string domain)
{
  DirectoryContext context = new DirectoryContext(
    DirectoryContextType.Domain,
    domain
    );
  
  DateTime latestLogon = DateTime.MinValue;
  string servername = null;
  
  DomainControllerCollection dcc = 
    DomainController.FindAll(context);
  
  foreach (DomainController dc in dcc)
  {
    DirectorySearcher ds;

    using (dc)
    using (ds = dc.GetDirectorySearcher())
    {
      ds.Filter = String.Format(
        "(sAMAccountName={0})",
        username
        );
      ds.PropertiesToLoad.Add("lastLogon");
      ds.SizeLimit = 1;
      
      SearchResult sr = ds.FindOne();
      
      if (sr != null)
      {
        DateTime lastLogon = DateTime.MinValue;
        if (sr.Properties.Contains("lastLogon"))
        {
          lastLogon = DateTime.FromFileTime(
            (long)sr.Properties["lastLogon"][0]
            );
        }
        
        if (DateTime.Compare(lastLogon,latestLogon) > 0)
        {
          latestLogon = lastLogon;
          servername = dc.Name;
        }
      }
    }
  }
  
  Console.WriteLine(
    "Last Logon: {0} at {1}",
    servername,
    latestLogon.ToString()
    );
}

We are using the SDS.AD namespace here to enumerate all of our domain controllers, and then we are using DirectorySearcher and SearchResult to marshal the LargeInteger syntax lastLogon attribute more easily. In domains with widely distributed domain controllers, we should be aware that network latency can slow this technique dramatically.

We should further keep in mind that this technique is relatively slow because it must bind to each domain controller in turn to find the user account and retrieve the lastLogon value. However, it is accurate, as each domain controller is searched and the latest logon is found.

Finding Stale Accounts

There is one more method we can use for finding old or unused accounts if we’re running Windows Server 2003 Active Directory in full Windows 2003 mode. The user class schema has been updated to add a new attribute called lastLogonTimestamp. This is a replicated value that is updated periodically. How often the attribute is updated depends on a new domain policy attribute named msDS-LogonTimeSyncInterval. By default, this value is 14 days, but it is configurable. As such, this attribute is not accurate for purposes of determining exactly the last time a user logged into the domain. We must use the lastLogon attribute when accuracy matters.

The syntax of this attribute is LargeInteger, so it is similar to other techniques we have already demonstrated. We can simply create a filter using the DateTime.ToFileTime method appropriately:

//find all users and computers that
//have not been used in 30 days
String filter = String.Format(
  "(&(objectClass=user)(lastLogonTimestamp<={0}))",
  DateTime.Now.Subtract(TimeSpan.FromDays(30)).ToFileTime()
  );

At first blush, this seems like a pretty easy thing to do. Indeed it is, if we can live with the following limitations.

  • It is inaccurate up to the value of msDS-LogonTimeSyncInterval (14 days by default). This means we can be no better than the resolution of this attribute in terms of our accuracy in finding accounts.
  • This is a Windows 2003 Active Directory–only feature, and even then, only when running in Windows 2003 mode.

Only NTLM and Kerberos logins update this attribute. Service Pack 1 must be applied to correct problems with NTLM as well. This means that any other type of operation that generates a login—certificates, custom Security Support Provider Interface (SSPI), Kerberos Service for User (S4U), and so on—will not update this attribute.

Additional caveats that depend on when the domain functional level was increased also affect the accuracy of this attribute. You can find further information about all the caveats at http://www.microsoft.com/technet/prodtechnol/windowsserver2003/library/TechRef/54094485-71f6-4be8-8ebf-faa45bc5db4c.mspx.

Determining Account Lockout

Determining whether an account is locked out is a strange odyssey. On the surface, it appears simple enough: We have a flag on userAccountControl called UF_LOCKOUT. Surely, that would mean that checking to see if the flag was flipped would tell us whether an account was locked out, right? Well, that really depends on which provider we use. As we mentioned earlier in this chapter, the userAccountControl attribute is inaccurate in terms of determining an account’s lockout status for the LDAP provider. The situation is somewhat better if we use the WinNT provider, as this version of the userAccountControl attribute accurately reflects lockout status. Microsoft was aware of this flakey implementation and fixed it on Windows 2003 Active Directory and ADAM with the msDS-User-Account-Control-Computed attribute. This constructed attribute will accurately reflect the UF_LOCKOUT flag for the LDAP provider. Listing 10.14 shows a sample of how this would work.

Listing 10.14: Determining Account Lockout

//user is a DirectoryEntry for our user account

string attrib = "msDS-User-Account-Control-Computed";

//this is a constructed attrib
user.RefreshCache(new string[]{attrib});

const int UF_LOCKOUT = 0x0010;

int flags = 
  (int)user.Properties[attrib].Value;

if (Convert.ToBoolean(flags & UF_LOCKOUT))
{
  Console.WriteLine(
    "{0} is locked out",
    user.Name
    );
}

There are a couple problems with this method, of course.

  • This works only for Windows 2003 Active Directory and ADAM, not for Windows 2000 Active Directory.
  • Since msDS-User-Account-Control-Computed is a constructed attribute, it cannot be used in an LDAP search filter.

Unfortunately, since this attribute cannot be used in a search filter, we really cannot use this method to find locked accounts proactively. Luckily, we can actually compute whether an account is locked out fairly accurately, and search for it. Previously in this chapter, we showed how we could determine the domain’s lockout duration policy. Used in conjunction with the lockoutTime attribute, we can accurately predict whether an account is locked out, and search for it. Listing 10.15 shows one such example.

Listing 10.15: Searching for Locked-Out Accounts

class Lockout : IDisposable
{
  DirectoryContext context;
  DirectoryEntry root;
  DomainPolicy policy;

  public Lockout(string domainName)
  {
    this.context = new DirectoryContext(
      DirectoryContextType.Domain,
      domainName
      );

    //get our current domain policy
    Domain domain = Domain.GetDomain(this.context);
    
    this.root = domain.GetDirectoryEntry();
    this.policy = new DomainPolicy(this.root);      
  }

  public void FindLockedAccounts()
  {
    //default for when accounts stay locked indefinitely
    string qry = "(lockoutTime>=1)";

    TimeSpan duration = this.policy.LockoutDuration;

    if (duration != TimeSpan.MaxValue)
    {
      DateTime lockoutThreshold =
        DateTime.Now.Subtract(duration);

      qry = String.Format(
        "(lockoutTime>={0})",
        lockoutThreshold.ToFileTime()
        );
    }
    
    DirectorySearcher ds = new DirectorySearcher(
      this.root,
      qry
      );

    using (SearchResultCollection src = ds.FindAll())
    {
      foreach (SearchResult sr in src)
      {
        long ticks =
          (long)sr.Properties["lockoutTime"][0];

        Console.WriteLine(
          "{0} locked out at {1}",
          sr.Properties["name"][0],
          DateTime.FromFileTime(ticks)
          );
      }
    }
  }
  
  public void Dispose()
  {
    if (this.root != null)
    {
      this.root.Dispose();
    }
  }
}

Listing 10.15 gives us a fairly simple way of finding accounts that have been locked. We are using the DomainPolicy helper class we introduced earlier in this chapter to read the lockoutDuration attribute on the root domain. If the attribute is set to TimeSpan.MaxValue, it means that an account is to stay locked until an administrator unlocks it. We are accounting for this policy possibly by setting our search filter to designate that any account with a lockoutTime with a nonzero value is locked out. This filter is appropriate only when our domain policy tells us that accounts are to be locked out indefinitely, until an administrator unlocks them.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020