Active Directory Tip #9 – How to escape the Path property

by Tobias Hertkorn on May 17th, 2010

Did you get the dreaded 0x80005000 COMException "Unknown error"? Maybe you should check, if your AD entry contains a forward slash '/' in its DN. Because there is a subtile difference between a correctly escaped distinguished name and a correctly escaped ldap path.

Observe:

PLAIN:
  1. Unescaped:                                 CN=Test\ Me \\ huhu \/ cool / \\/ // \\// \\\/// \\\\////! /////,OU=AdTests,DC=test,DC=domain,DC=local
  2. Escaped DN:                                CN=Test\\ Me \\\\ huhu \\/ cool / \\\\/ // \\\\// \\\\\\/// \\\\\\\\////! /////,OU=AdTests,DC=test,DC=domain,DC=local
  3. Escaped Path: LDAP://test.domain.local:389/CN=Test\\ Me \\\\ huhu \\\/ cool \/ \\\\\/ \/\/ \\\\\/\/ \\\\\\\/\/\/ \\\\\\\\\/\/\/\/! \/\/\/\/\/,OU=AdTests,DC=test,DC=domain,DC=local

Notice how forward slashes need to be escaped in addition to how distinguished names get escaped. This is a very subtile thing that is often overlooked. So when converting between DNs and Paths you have to use the following methods:

C#:
  1. private static readonly string LDAP_PREFIX = @"LDAP://test.domain.local:389/";
  2.  
  3. private static string ConvertDnToPath(string dn)
  4. {
  5.   var exploded = dn.ToCharArray();
  6.   StringBuilder sb = new StringBuilder();
  7.  
  8.   foreach (var @char in exploded)
  9.   {
  10.     if (@char == '/')
  11.     {
  12.       sb.Append('\\');
  13.     }
  14.     sb.Append(@char);
  15.   }
  16.  
  17.   return LDAP_PREFIX + sb.ToString();
  18. }
  19.  
  20.  
  21.  
  22. private static string ConvertPathToDn(string path)
  23. {
  24.   path = path.Substring(path.IndexOf('/', "LDAP://".Length));
  25.   // remove leading /
  26.   while (path.StartsWith("/"))
  27.   {
  28.     path = path.Substring(1);
  29.   }
  30.  
  31.  
  32.   if (!path.Contains('/'))
  33.   {
  34.     return path;
  35.   }
  36.  
  37.   StringBuilder sb = new StringBuilder(path);
  38.  
  39.   bool slashMode = false;
  40.   int backslashCount = 0;
  41.   for (int i = path.Length - 1; i>= 0; i--)
  42.   {
  43.     if (path[i] == '\\' && slashMode)
  44.     {
  45.       backslashCount++;
  46.     }
  47.     else if (slashMode)
  48.     {
  49.       if ((backslashCount % 2) != 0)
  50.       {
  51.         sb.Remove(i + 1, 1);
  52.       }
  53.       backslashCount = 0;
  54.       slashMode = false;
  55.     }
  56.  
  57.     if (path[i] == '/')
  58.     {
  59.       backslashCount = 0;
  60.       slashMode = true;
  61.     }
  62.   }
  63.  
  64.   return sb.ToString();
  65. }

Post to Twitter Tweet this

May 17th, 2010 9:00 pm | Comments (0)

Active Directory Tip #8 – Set Password of user

by Tobias Hertkorn on May 14th, 2010

The password of a person entry is not a regular property with direct access for reading and writing. Instead it can't be read just indirectly written to via the setPassword method.

C#:
  1. string password = "secret";
  2. directoryEntry.Invoke("setPassword", password);
  3. directoryEntry.RefreshCache();

The caller must have the User-Force-Change-Password Extended Right to set the password with this method.

Post to Twitter Tweet this

May 14th, 2010 8:21 pm | Comments (0)

Active Directory Tip #7 – Handling a Bit Property (e.g. userAccountControl – Account is disabled)

by Tobias Hertkorn on May 12th, 2010

Bit properties like User-Account-Control Attribute in the Active Directory are represented by regular integers. Therefore all the usual bit operation may be performed on those fields.

C#:
  1. public static bool GetBitAttribute(DirectoryEntry directoryEntry, string attributeName, int bitmask)
  2. {
  3.   int value = (int)DirectoryEntryHelper.GetAdObjectProperty(directoryEntry, attributeName);
  4.   bool result = (bitmask & value) == bitmask;
  5.   return result;
  6. }
  7.  
  8. public static void SetBitAttribute(DirectoryEntry directoryEntry, string attributeName, int bitmask, bool attributeValue)
  9. {
  10.   PropertyValueCollection property = directoryEntry.Properties[attributeName];
  11.   int value = (int)property.Value;
  12.   if (attributeValue)
  13.   {
  14.     value |= bitmask;
  15.   }
  16.   else
  17.   {
  18.     value &= ~bitmask;
  19.   }
  20.  
  21.   property.Value = value;
  22. }

With these helper functions deactivating a user would be accomplished by using

C#:
  1. SetBitAttribute(directoryEntry, "userAccountControl", ADS_UF_ACCOUNTDISABLE, true);

Just for convenience, here is the table form User-Account-Control Attribute converted to C# syntax:

C#:
  1. /// <summary>
  2. /// The logon script is executed.
  3. /// </summary>
  4. int ADS_UF_SCRIPT = 0x00000001;
  5. /// <summary>
  6. /// The user account is disabled.
  7. /// </summary>
  8. int ADS_UF_ACCOUNTDISABLE = 0x00000002;
  9. /// <summary>
  10. /// The home directory is required.
  11. /// </summary>
  12. int ADS_UF_HOMEDIR_REQUIRED = 0x00000008;
  13. /// <summary>
  14. /// The account is currently locked out.
  15. /// </summary>
  16. int ADS_UF_LOCKOUT = 0x00000010;
  17. /// <summary>
  18. /// No password is required.
  19. /// </summary>
  20. int ADS_UF_PASSWD_NOTREQD = 0x00000020;
  21. /// <summary>
  22. /// The user cannot change the password.
  23. /// </summary>
  24. int ADS_UF_PASSWD_CANT_CHANGE = 0x00000040;
  25. /// <summary>
  26. /// The user can send an encrypted password.
  27. /// </summary>
  28. int ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080;
  29. /// <summary>
  30. /// This is an account for users whose primary account is in
  31. /// another domain. This account provides user access to this
  32. /// domain, but not to any domain that trusts this domain.
  33. /// Also known as a local user account.
  34. /// </summary>
  35. int ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100;
  36. /// <summary>
  37. /// This is a default account type that represents a typical user.
  38. /// </summary>
  39. int ADS_UF_NORMAL_ACCOUNT = 0x00000200;
  40. /// <summary>
  41. /// This is a permit to trust account for a system domain that
  42. /// trusts other domains.
  43. /// </summary>
  44. int ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800;
  45. /// <summary>
  46. /// This is a computer account for a computer that is a member
  47. /// of this domain.
  48. /// </summary>
  49. int ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000;
  50. /// <summary>
  51. /// This is a computer account for a system backup domain controller
  52. /// that is a member of this domain.
  53. /// </summary>
  54. int ADS_UF_SERVER_TRUST_ACCOUNT = 0x00002000;
  55. /// <summary>
  56. /// The password for this account will never expire.
  57. /// </summary>
  58. int ADS_UF_DONT_EXPIRE_PASSWD = 0x00010000;
  59. /// <summary>
  60. /// This is an MNS logon account.
  61. /// </summary>
  62. int ADS_UF_MNS_LOGON_ACCOUNT = 0x00020000;
  63. /// <summary>
  64. /// The user must log on using a smart card.
  65. /// </summary>
  66. int ADS_UF_SMARTCARD_REQUIRED = 0x00040000;
  67. /// <summary>
  68. /// The service account (user or computer account), under which a
  69. /// service runs, is trusted for Kerberos delegation. Any such service
  70. /// can impersonate a client requesting the service.
  71. /// </summary>
  72. int ADS_UF_TRUSTED_FOR_DELEGATION = 0x00080000;
  73. /// <summary>
  74. /// The security context of the user will not be delegated to a service
  75. /// even if the service account is set as trusted for Kerberos delegation.
  76. /// </summary>
  77. int ADS_UF_NOT_DELEGATED = 0x00100000;
  78. /// <summary>
  79. /// Restrict this principal to use only Data Encryption Standard (DES)
  80. /// encryption types for keys.
  81. /// </summary>
  82. int ADS_UF_USE_DES_KEY_ONLY = 0x00200000;
  83. /// <summary>
  84. /// This account does not require Kerberos pre-authentication for logon.
  85. /// </summary>
  86. int ADS_UF_DONT_REQUIRE_PREAUTH = 0x00400000;
  87. /// <summary>
  88. /// The user password has expired. This flag is created by the system
  89. /// using data from the Pwd-Last-Set attribute and the domain policy.
  90. /// </summary>
  91. int ADS_UF_PASSWORD_EXPIRED = 0x00800000;
  92. /// <summary>
  93. /// The account is enabled for delegation. This is a security-sensitive
  94. /// setting; accounts with this option enabled should be strictly
  95. /// controlled. This setting enables a service running under the account
  96. /// to assume a client identity and authenticate as that user to other
  97. /// remote servers on the network.
  98. /// </summary>
  99. int ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000;

Post to Twitter Tweet this

May 12th, 2010 8:14 pm | Comments (0)

Active Directory Tip #6 – Set Account never expires

by Tobias Hertkorn on May 10th, 2010

The account never expires checkbox in the MMC is a virtual option. Instead of being a separate property it is set using the accountExpires property. Setting it to the equivalent of 0 will result in an account that never expires.

See Account-Expires Attribute for more details.

C#:
  1. /// <summary>
  2. /// You still have to call CommitChanges() after using this method.
  3. /// </summary>
  4. private void SetAccountNeverExpires(DirectoryEntry directoryEntry)
  5. {
  6.   long accountExpires = 0;
  7.    
  8.   var accountExpiresOriginalValue = directoryEntry.Properties["accountExpires"].Value;
  9.  
  10.   Type type = accountExpiresOriginalValue.GetType();
  11.  
  12.   int highPart = (int)(accountExpires>> 32);
  13.   int lowPart = (int)(0xFFFFFFFF & accountExpires);
  14.  
  15.   type.InvokeMember("HighPart", BindingFlags.SetProperty, null, accountExpiresOriginalValue, new object[] { highPart });
  16.   type.InvokeMember("LowPart", BindingFlags.SetProperty, null, accountExpiresOriginalValue, new object[] { lowPart });
  17.  
  18.   SetAdObjectAttribute(directoryEntry, "accountExpires", accountExpiresOriginalValue);
  19. }

Post to Twitter Tweet this

May 10th, 2010 9:02 pm | Comments (0)

Active Directory Tip #5 – Set Large Integer (e.g. Account expires date)

by Tobias Hertkorn on May 10th, 2010

Setting a large integer property is just as easy.

C#:
  1. /// <summary>
  2. /// You still have to call CommitChanges() after using this method.
  3. /// </summary>
  4. private void SetAccountExpiresDate(DirectoryEntry directoryEntry, DateTime expiresOn)
  5. {
  6.   long accountExpires = expiresOn.ToFileTimeUtc();
  7.    
  8.   var accountExpiresOriginalValue = directoryEntry.Properties["accountExpires"].Value;
  9.  
  10.   Type type = accountExpiresOriginalValue.GetType();
  11.  
  12.   int highPart = (int)(accountExpires>> 32);
  13.   int lowPart = (int)(0xFFFFFFFF & accountExpires);
  14.  
  15.   type.InvokeMember("HighPart", BindingFlags.SetProperty, null, accountExpiresOriginalValue, new object[] { highPart });
  16.   type.InvokeMember("LowPart", BindingFlags.SetProperty, null, accountExpiresOriginalValue, new object[] { lowPart });
  17.  
  18.   SetAdObjectAttribute(directoryEntry, "accountExpires", accountExpiresOriginalValue);
  19. }

Post to Twitter Tweet this

May 10th, 2010 2:00 pm | Comments (0)

Active Directory Tip #4 – Get Large Integer (e.g. Account expires date)

by Tobias Hertkorn on May 4th, 2010

Certain properties, like usnChanged or accountExpires, are in an IADsLargeInteger format. One way to read out the long representation of the value of such a property is by reflection.

C#:
  1. private static long ConvertLargeIntegerToLong(object largeInteger)
  2. {
  3.   Type type = largeInteger.GetType();
  4.  
  5.   int highPart = (int)type.InvokeMember("HighPart", BindingFlags.GetProperty, null, largeInteger, null);
  6.   int lowPart = (int)type.InvokeMember("LowPart", BindingFlags.GetProperty | BindingFlags.Public, null, largeInteger, null);
  7.  
  8.   return (long)highPart <<32 | (uint)lowPart;
  9. }

That value may be converted to a DateTime value using the static DateTime.FromFileTimeUtc method.

C#:
  1. object accountExpires = DirectoryEntryHelper.GetAdObjectProperty(directoryEntry, "accountExpires");
  2. var asLong = ConvertLargeIntegerToLong(accountExpires);
  3.  
  4. if (asLong == long.MaxValue || asLong <= 0 || DateTime.MaxValue.ToFileTime() <= asLong)
  5. {
  6.   return DateTime.MaxValue;
  7. }
  8. else
  9. {
  10.   return DateTime.FromFileTimeUtc(asLong);
  11. }

Post to Twitter Tweet this

May 4th, 2010 10:58 pm | Comments (0)

Active Directory Tip #3 – Get DirectoryEntry Property with range fetch

by Tobias Hertkorn on May 2nd, 2010

Tip #2 already dealt with one of the problems while using multi-value properties.But there is another pitfall that got me once - and usually only happens in production environments and is very hard to debug. I am talking about property range fetch a mechanism introduced to allow the active directory server to use certain optimizations. Unfortunatelly the optimization means a harder to understand API and more work for us programmer.

When dealing with multi-value properties with a huge number of values the active directory server will only return a certain amount of values on the first access (usually 1500 items). On some properties these values don't have an order so that there is a certain randomness to which values are returned - and therefore a certain randomness if your code works as expected or not. Which makes spotting the problem so hard.

So instead of relying on the fact that directoryEntry.Properties["member"] will return all values one must use the range fetch method using DirectorySearcher in order to get all values for said property. In the code sample below I used a cutoff value to determine if simple access should be trusted or if range fetch (slower) must be used.

C#:
  1. public static readonly int ATTRIBUTE_RANGE_FETCH_CUTOFF = 500;
  2.  
  3. public static List<object> GetAdObjectProperties(DirectoryEntry entry, string propertyName)
  4. {
  5.   if (entry == null) { throw new ArgumentNullException("entry"); }
  6.   if (string.IsNullOrEmpty(propertyName)) { throw new ArgumentException("propertyName"); }
  7.  
  8.   List<object> result = new List<object>();
  9.  
  10.   if (entry.Properties.Contains(propertyName))
  11.   {
  12.     int count = entry.Properties[propertyName].Count;
  13.  
  14.     if (count == 0)
  15.     {
  16.       // Nothing to do.
  17.     }
  18.     if (count == 1)
  19.     {
  20.       result.Add(entry.Properties[propertyName].Value.ToString());
  21.     }
  22.     else if (count <ATTRIBUTE_RANGE_FETCH_CUTOFF)
  23.     {
  24.       foreach (object value in entry.Properties[propertyName])
  25.       {
  26.         result.Add(value.ToString());
  27.       }
  28.     }
  29.     else
  30.     {
  31.       return GetAdObjectPropertiesWithRangeFetch(entry, propertyName);
  32.     }
  33.   }
  34.  
  35.   return result;
  36. }
  37.  
  38. private static List<object> GetAdObjectPropertiesWithRangeFetch(DirectoryEntry entry, string propertyName)
  39. {
  40.   List<object> result = new List<object>();
  41.   int index = 0;
  42.  
  43.   int step = entry.Properties[propertyName].Count - 1;
  44.   string rangeFormatString = propertyName + ";range={0}-{1}";
  45.  
  46.   string currentRange = string.Format(rangeFormatString, 0, step);
  47.  
  48.   using (DirectorySearcher searcher = new DirectorySearcher(entry, string.Format("({0}=*)", propertyName), new string[] { currentRange }, SearchScope.Base))
  49.   {
  50.     SearchResult searchResult = FindCurrentRange(searcher, currentRange);
  51.  
  52.     while ((searchResult != null) && searchResult.Properties.Contains(currentRange))
  53.     {
  54.       foreach (object item in searchResult.Properties[currentRange])
  55.       {
  56.         if (item != null)
  57.         {
  58.           result.Add(item);
  59.         }
  60.         index++;
  61.       }
  62.  
  63.       currentRange = string.Format(rangeFormatString, index, (index + step));
  64.  
  65.       searchResult = FindCurrentRange(searcher, currentRange);
  66.     }
  67.  
  68.     if (searchResult != null)
  69.     {
  70.       // final search with '*' as upper limit
  71.       string finalRange = string.Format(rangeFormatString, index, "*");
  72.       searchResult = FindCurrentRange(searcher, finalRange);
  73.  
  74.       foreach (object item in searchResult.Properties[finalRange])
  75.       {
  76.         if (item != null)
  77.         {
  78.           result.Add(item);
  79.         }                 
  80.       }
  81.     }
  82.  
  83.     return result;
  84.   }
  85. }
  86.  
  87. private static SearchResult FindCurrentRange(DirectorySearcher searcher, string currentRange)
  88. {
  89.   searcher.PropertiesToLoad.Clear();
  90.   searcher.PropertiesToLoad.Add(currentRange);
  91.  
  92.   SearchResult searchResult = searcher.FindOne();
  93.   return searchResult;
  94. }

Post to Twitter Tweet this

May 2nd, 2010 10:55 pm | Comments (0)

Active Directory Tip #2 – Get DirectoryEntry Property as object list

by Tobias Hertkorn on April 30th, 2010

Even when you have a directory entry, it's not very easy to interact with it. Especially using the Properties property of the DirectoryEntry class is not as intuitive as I wish it would be. It either acts as a List, when there are more than one value to return, or it acts as a single value property when there is only one value currently set. That means accessing multi value ad properties like the url property through directoryEntry.Properties will act differently depending on how many values are actually set on the active directory entry.

To be save from these kinds of edge-cases it is best to not directly use the Properties property, but instead use a helper function. That way it always behaves like a list.

C#:
  1. public static List<object> GetAdObjectProperties(DirectoryEntry entry, string propertyName)
  2. {
  3.   if (entry == null) { throw new ArgumentNullException("entry"); }
  4.   if (string.IsNullOrEmpty(propertyName)) { throw new ArgumentException("propertyName"); }
  5.  
  6.   List<object> result = new List<object>();
  7.  
  8.   if (entry.Properties.Contains(propertyName))
  9.   {
  10.     int count = entry.Properties[propertyName].Count;
  11.  
  12.     if (count == 0)
  13.     {
  14.       // nothing to do
  15.     }
  16.     else if (count == 1)
  17.     {
  18.       result.Add(entry.Properties[propertyName].Value);
  19.     }
  20.     else
  21.     {
  22.       foreach (object value in entry.Properties[propertyName])
  23.       {
  24.         result.Add(value);
  25.       }
  26.     }
  27.   }
  28.  
  29.   return result;
  30. }

Unfortunatelly this is not all that needs to be observed while reading properties. Make sure to check out Tip #3 to find out more.

Post to Twitter Tweet this

April 30th, 2010 10:54 pm | Comments (0)

Active Directory Tip #1 – DirectoryEntry.Exists with authentication

by Tobias Hertkorn on April 28th, 2010

When checking, if an entry in the Active Directory exists, usually I would use the static DirectoryEntry.Exists method. Unfortunatelly it does not provide an overload that lets me specify an username and password. But that's easily fixed. Just check for the error code "no such object" in the COMException that is thrown in case of a Ldap path that points to no existing object.

C#:
  1. public static bool Exists(string ldapPath, string username, string password)
  2. {
  3.   return GetDirectoryEntry(ldapPath, username, password) != null;
  4. }
  5.  
  6. private static readonly int ERROR_DS_NO_SUCH_OBJECT =  -2147016656;
  7.  
  8. public static DirectoryEntry GetDirectoryEntry(string ldapPath, string username, string password)
  9. {
  10.   DirectoryEntry de = new DirectoryEntry();
  11.   de.Path = ldapPath;
  12.   de.Username = username;
  13.   de.Password = password;
  14.   de.AuthenticationType = AuthenticationTypes.Secure;
  15.  
  16.   try
  17.   {
  18.     if (de.NativeObject == null)
  19.     {
  20.       // No AD object found!
  21.       de.Close();
  22.       de = null;
  23.     }
  24.   }
  25.   catch (COMException exception)
  26.   {
  27.     // 0x80072030 ERROR_DS_NO_SUCH_OBJECT
  28.     // http://msdn.microsoft.com/en-us/library/aa746386.aspx
  29.     if (exception.ErrorCode != ERROR_DS_NO_SUCH_OBJECT)
  30.     {
  31.       throw;
  32.     }
  33.     de.Close();
  34.     de = null;
  35.   }
  36.  
  37.   return de;
  38. }

Post to Twitter Tweet this

April 28th, 2010 10:59 pm | Comments (2)

Active Directory Tips – The series

by Tobias Hertkorn on April 28th, 2010

I am currently digging into old automation code for active directory object CRUD. Unfortunatelly there are certain pitfalls and weird edge-cases surrounding the use of DirectoryEntry. So in order to save me the research in future projects and maybe help you I am starting a small series focusing on those pitfalls and edge-cases. I call them "tips" because it will be mostly code and less explanation of the code. Feel free to use the comments if you have further questions concerning the code. Just remember: If you have a question chances are somebody wants the answer as well.

Post to Twitter Tweet this

April 28th, 2010 10:51 pm | Comments (0)
Tobi + C# = T# - Blogged blogoscoop