COM Interop Data Types
We have already established that certain types of attributes require some form of COM interop with ADSI in order to use these values from PropertyValueCollection, and we know why this happens. Now we will focus on the specifics for dealing with these types of attributes.
It is very helpful (and assumed for this book) that you have at least a basic understanding of COM interop in .NET. Specifically, we are talking about using a runtime-callable wrapper (RCW), a mechanism by which the .NET Framework can call into COM code.
Approaches for COM Interop
Essentially three different mechanisms are available for dealing with COM interop with ADSI.
- Create an RCW interop assembly with tlbimp.exe, or by setting a COM reference to activeds.tlb in Visual Studio .NET.
- Create our own interop assembly by hand, adding the specific type declarations needed to use the types in which we are interested.
- Use .NET reflection to late-bind to the ADSI types we wish to use.
We expand on these in detail in Appendix A. The code in this chapter shows a mix of the interop assembly and reflection approaches.
Note: DirectorySearcher Marshals These Types Differently
As we cover the following COM interop data types, we should keep in mind that DirectorySearcher marshals these values differently than DirectoryEntry, as previously explained. Where appropriate, we will attempt to show the differences between these classes when reading the COM interop types.
LargeInteger Values
Of the syntaxes that require COM interop, the LargeInteger syntax, 2.5.5.16, is the most common. A LargeInteger value is just an 8-byte integer. Active Directory uses this attribute syntax frequently for storing date/ time values as well as the large numbers used for replication sequencing. ADSI returns this syntax using the IADsLargeInteger interface.
This interface exists primarily because the automation-compatible languages such as Visual Basic and VBScript do not support 8-byte integers directly. The ADSI designers solved this problem by returning a special type, IADsLargeInteger, which contains the eight bytes of data split into two properties, the HighPart and the LowPart, each containing four bytes of data in a standard VT_I4 variant that represents a 4-byte signed integer.
As developers, we are a little bummed that this type is not automatically marshaled directly into a System.Int64 for us using DirectoryEntry, given that .NET can easily support 8-byte integers.
Warning: LowPart Is Not Really a Signed Integer!
It seems like a very natural thing to fit one 8-byte integer into two 4-byte integers. We just split up the binary data into a HighPart and a LowPart and we are all set.
The problem is that both HighPart and LowPart are treated as normal signed integers, since that is what the COM variant types support. However, in reality, LowPart is not really a signed integer at all, but simply four bytes of data, or an unsigned integer at best. The main difference between a signed integer and an unsigned integer is that in a signed integer, the high bit indicates positive or negative, whereas in an unsigned integer, it just increases the value.
Thus, we cannot treat the sign of the LowPart as meaningful. Do not try to use addition to combine the values, or you may get intermittent surprises. If you see another sample that uses addition, it probably has a bug.
So, how do we actually read the value? Listing 6.1 shows how to do this in C#, with the reflection-based approach thrown in for good measure.
Listing 6.1. Converting IADsLargeInteger to System.Int64 in C#
using System.DirectoryServices; using ActiveDs; DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); using (entry) { object val = entry.Properties["lockoutDuration"].Value; Console.WriteLine( "Using ActiveDs Interop: {0}", GetInt64(val) ); Console.WriteLine( "Using Reflection: {0}", LongFromLargeInteger(val) ); } //using the RCW interop static Int64 GetInt64(object largeIntVal) { if (largeIntVal == null) throw new ArgumentNullException("largeIntVal"); IADsLargeInteger largeInt; largeInt = (IADsLargeInteger) largeIntVal; return (long)largeInt.HighPart << 32 | (uint)largeInt.LowPart; } //decodes IADsLargeInteger objects into Int64 format (long) //using Reflection instead of Interop static long LongFromLargeInteger(object largeInteger) { System.Type type = largeInteger.GetType(); int highPart = (int)type.InvokeMember( "HighPart", BindingFlags.GetProperty, null, largeInteger, null ); int lowPart = (int)type.InvokeMember( "LowPart", BindingFlags.GetProperty, null, largeInteger, null ); return (long)highPart << 32 | (uint)lowPart; } |
In Listing 6.1, we showed two equivalent methods for decoding IADsLargeInteger into a System.Int64 (long). One method uses the RCW for activeds.tlb to create a strongly typed IADsLargeInteger, and the other method uses reflection to invoke back into the type at runtime and get the same properties. Notice how both functions use bit shifting and a cast to uint to convert the value? This is the proper approach. However, there is a problem if we are using Visual Basic .NET, as we may not have support for unsigned integers, depending on our version of the .NET Framework. Support for unsigned integers was only added to Visual Basic .NET in version 2.0 of the .NET Framework. In order to be fair, we include Listing 6.2, which will work in any version.
Listing 6.2. Converting IADsLargeInteger to System.Int64 in Visual Basic .NET
Imports ActiveDs Function ConvertToInt64(ByVal largeInt As Object) as Int64 Dim data(7) As Byte Dim adsLargeInt As IADsLargeInteger adsLargeInt = Ctype(largeInt, IADsLargeInteger) BitConverter.GetBytes(adsLargeInt.LowPart).CopyTo(data, 0) BitConverter.GetBytes(adsLargeInt.HighPart).CopyTo(data, 4) return BitConverter.ToInt64(data, 0) End Function |
This one is going to seem a little weird at first. In order to get around the issues with unsigned integers in Visual Basic .NET, we use the BitConverter class to treat the data as raw binary. We essentially copy the individual byte arrays representing the component pieces to a new byte array, and convert that back to an Int64 in the same way. Note that we copy the low part into the beginning of the array, as integer values on Intel platforms are "little endian."
Converting Int64 to DateTime
Oftentimes, we can represent the IADsLargeInteger that we convert to Int64 by either a DateTime or a TimeSpan in .NET. Depending on the attribute, we just need to use one of the static methods on the System.DateTime class (FromFileTime, FromFileTimeUtc) or TimeSpan (FromTicks) to do the conversion easily. We will have to know at some level whether the attribute represents an actual date/time (e.g., lockoutTime), or a duration (e.g., lockoutDuration), to decide which class to use for the conversion. Regardless, we must make sure to test the value to ensure that it is greater than zero (> 0) before passing it to the DateTime methods; otherwise, an exception will be thrown. Some attributes, like pwdLastSet, use 0 to indicate states other than an actual time, so caution must be used.
Using DirectorySearcher
When dealing with LargeInteger syntax attributes, it is often easier to use the DirectorySearcher marshaling behavior as opposed to using the IADsLargeInteger interface with DirectoryEntry. It returns an Int64 object directly, which is typically all we want in the first place. If we do not currently have a SearchResult, we can just use a base-scoped search on our DirectoryEntry and avoid the whole interop mess, as shown in Listing 6.3.
Listing 6.3. Reading LargeInteger Syntax with DirectorySearcher
//use a base-scoped search DirectorySearcher ds = new DirectorySearcher( user, //this is our targeted entry "(objectClass=*)", //base level filter null, SearchScope.Base ); //we essentially convert our DirectoryEntry //to a SearchResult to get the desired behavior SearchResult result = ds.FindOne(); //now we get the different marshaling //behavior we want (no IADsLargeInteger) long usnChanged = (long) result.Properties["usnChanged"][0]; //or... Int64 usnChanged = (Int64) result.Properties["usnChanged"][0]; |
Whenever possible, we should choose to use DirectorySearcher and SearchResult to read these attribute types. Trust us; it will make your life easier.
DN-With-Binary
The DN-With-Binary syntax, 2.5.5.7, is fairly rare and is primarily used by the wellKnownObjects and otherWellKnownObjects attributes in Active Directory. As we discussed in Chapter 3, when we talked about binding using WKGUID, these types of attributes associate a GUID value (the binary part) with a DN to allow an object to be identified by a published GUID value and still have a localized name in the DN.
DN-With-Binary syntax attributes are marshaled by the DirectoryEntry classes as System.__ComObjects that can be cast to an IADsDNWithBinary ADSI object. The interface is very simple and has no significant gotchas, as IADsLargeInteger does.
- The BinaryValue property contains the binary part and is marshaled as a byte[].
- The DNString property contains the DN part and is marshaled as a simple string.
Listing 6.4 shows an example of how we can get these two properties.
Listing 6.4. Converting DN-With-Binary for WellKnownObjects
string adsPath = "LDAP://dc=domain,dc=com"; DirectoryEntry root = new DirectoryEntry( adsPath, null, null, AuthenticationTypes.Secure ); using (root) { if (root.Properties.Contains("wellKnownObjects")) { foreach(object o in root.Properties["wellKnownObjects"]) { byte[] guidBytes; string dn; //use our helper function DecodeDnWithBinary( o, out guidBytes, out dn ); Console.WriteLine( "Guid: {0}", new Guid(guidBytes).ToString("p") ); Console.WriteLine( "DN: {0}", dn ); } } } //This is our reflection-based helper for //decoding DN-With-Binary attributes private void DecodeDnWithBinary( object dnWithBinary, out byte[] binaryPart, out string dnString) { System.Type type = dnWithBinary.GetType(); binaryPart = (byte[])type.InvokeMember( "BinaryValue", BindingFlags.GetProperty, null, dnWithBinary, null ); dnString = (string)type.InvokeMember( "DNString", BindingFlags.GetProperty, null, dnWithBinary, null ); } |
Similar to what we can do with IADsLargeInteger, we can use the ActiveDs RCW interop approach, or we can simply use reflection to pull both the BinaryValue and DNString properties at runtime. We chose to show the reflection-based approach because this reduces runtime dependencies when deploying our application, but either approach is totally valid.
Note: DN-With-String
Active Directory and ADAM also support another syntax, called DN-With-String, which is essentially just like DN-With-Binary, except that an arbitrary string is associated with the DN value instead of some binary data. We decided not to bother to show this because there isn't a single attribute in the Active Directory 2003 schema (Exchange Server 2003 installed and all) that actually uses the syntax. You would have to create your own in order to need to know how to do this. If you do, the mechanics are the same as those used with DN-With-Binary.
Using DirectorySearcher
The marshaling behavior on this type is interesting, as it is actually marshaled as a delimited string. We can simply parse the value to retrieve the GUID and DN values. Listing 6.5 shows one such example.
Listing 6.5. Reading DN-With-Binary with DirectorySearcher
DirectoryEntry root = new DirectoryEntry( "LDAP://dc=domain,dc=com", null, null, AuthenticationTypes.Secure ); using (root) { DirectorySearcher ds = new DirectorySearcher( root, "(objectClass=*)", new string[]{"wellKnownObjects"}, SearchScope.Base ); SearchResult sr = ds.FindOne(); if (sr != null) { foreach (string s in sr.Properties["wellKnownObjects"]) { string[] parts = s.Split(new char[]{':'}); Console.WriteLine( "Guid: {0}", new Guid(parts[2]).ToString("b") ); Console.WriteLine( "DN: {0}", parts[3] ); } } } |
It can be advantageous to use DirectorySearcher, simply because we will not have to worry about the whole COM interop mess again. We just need to worry about safely parsing the returned string values.
Reading Security Descriptors
Security descriptors are interesting objects in Active Directory and ADAM. A security descriptor is actually a complex structure that contains several pieces, including a discretionary access control list (DACL) used for determining permissions on the object and a system access control list (SACL) that determines auditing behavior. However, the security descriptor itself is stored and retrieved as a single attribute value.
The really interesting part in all of this is that different types of users have different levels of access to the various parts of the security descriptor. For example, only administrators can read the SACL. Normal users cannot even see it!
So, if the entire security descriptor is read as a single piece of data, but only certain users can see the SACL, how exactly does that work? No other attributes in the directory allow us to see only certain parts based on our permissions, so this is a behavior we have not experienced so far.
The secret to making this work involves a special LDAP control that indicates to the directory what parts of the security descriptor should be returned as the result of a search. We may specify any combination of the owner, group, DACL, or SACL.
Here is where it gets tricky: If we request to read the security descriptor but ask for a part that we do not have access to (e.g., the SACL), it will silently fail and nothing will be retrieved at all. Additionally, if the special LDAP control is not used on the search, then the default behavior is to try to return the entire security descriptor. This will fail if we are not an administrator, as we do not have rights to read the SACL!
Note: Special Issues with Reading Security Descriptors with ADAM Principals
The actual problem with reading the SACL portion of the security descriptor is that it requires the ACCESS_SYSTEM_SECURITY privilege in the user's security token. Since this is given only to administrators by default, this effectively limits reading SACLs to administrators. ADAM users do not even have traditional Windows security tokens with privileges, so this effectively prevents an ADAM security principal from ever being able to read a SACL (or gain all administrative rights to an ADAM instance). According to our sources, Microsoft is looking at ways to solve this problem in a future version of ADAM.
The DirectoryEntry object makes this easy for us. By default, the underlying ADSI layer will automatically add the special control to search requests for the security descriptor and specify that everything but the SACL is returned. This default behavior gives us what we want nearly all the time, as we would rather have most of the security descriptor (and arguably the most important part: the DACL) than nothing at all. The downside here is that if we are actually binding to the directory with an administrative account and we really did want to read the SACL as well, we have to do some extra work.
The extra work is performed using the IADsObjectOptions interface, or by using the DirectoryEntryConfiguration class in .NET 2.0 or the Invoke method in .NET 1.x. All of these types provide ways of specifying which parts of the security descriptor should be returned. Listing 6.6 shows an example.
Listing 6.6. Setting Security Masks for DirectoryEntry
//given a DirectoryEntry entry that we will //retrieve the security descriptor on and wish //to get the SACL for as well //this approach works with .NET 2.0 entry.Options.SecurityMasks = SecurityMasks.Owner | SecurityMasks.Group | SecurityMasks.Dacl | SecurityMasks.Sacl; //this approach works with any version of the framework //but must be used on .NET 1.x const int ADS_OPTION_SECURITY_MASK = 3; const int ADS_SECURITY_INFO_OWNER = 0x1; const int ADS_SECURITY_INFO_GROUP = 0x2; const int ADS_SECURITY_INFO_DACL = 0x4; const int ADS_SECURITY_INFO_SACL = 0x8; int flags = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_GROUP | ADS_SECURITY_INFO_DACL | ADS_SECURITY_INFO_SACL; entry.Invoke("SetOption", new object[] {ADS_OPTION_SECURITY_MASK, flags} ); |
The behavior of DirectorySearcher is slightly different here. By default, it does not load the special control to modify security descriptor read behavior. As a result, the entire security descriptor is requested and we will receive a security descriptor only if we are bound as an administrator. In .NET 1.x, there is no way to modify this behavior.
.NET 2.0 addresses this by adding the ability to supply the security descriptor flags to DirectorySearcher. Chapter 5 contains a sample of how this is done.
Table 6.2 summarizes the behavior.
Type |
Version |
Default Security Descriptor Flags |
Modifiable Behavior? |
---|---|---|---|
DirectoryEntry |
1.x |
owner, group, and DACL |
Yes (Invoke method) |
DirectorySearcher |
1.x |
ALL (no control sent) |
No |
DirectoryEntry |
2.0 |
owner, group, and DACL |
Yes (DirectoryEntryConfiguration) |
DirectorySearcher |
2.0 |
ALL (no control sent) |
Yes (SecurityMasks property) |
Data Types for Security Descriptors
Once again, the DirectoryEntry and DirectorySearcher classes behave differently here. When we read the ntSecurityDescriptor attribute directly from PropertyValueCollection, DirectoryEntry marshals security descriptors as a System.__ComObject data type containing an IADsSecurityDescriptor ADSI interface. This interface uses some additional ADSI interfaces to allow us to manage access control lists (ACLs) on directory objects. Access control itself is a fairly big topic that we cover in more detail in Chapter 8. Here, we will just show how the value is obtained using COM interop.
DirectorySearcher marshals security descriptors in binary format as a byte array. Various options are available to convert the binary format into something we can work with. See Chapter 8 again for more details.
The downside of the DirectoryEntry class' behavior is that we must use COM interop to work with this data type. .NET 2.0 solves this problem with a new property on the DirectoryEntry class, called ObjectSecurity. It allows us to read and write security descriptors using the new .NET managed access control list (MACL) classes. We will definitely prefer to use these new classes when possible. One important thing to note, though, is that this new property supports only the actual security descriptor on the object. If another attribute contains a security descriptor, as might be the case with some Exchange Server attributes, we will need to go back to COM interop.
Listing 6.7 shows a basic example.
Listing 6.7. Reading Security Descriptors
using System.DirectoryServices; using ActiveDs; DirectoryEntry entry; entry = new DirectoryEntry( "LDAP://DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); using (entry) { IADsSecurityDescriptor sd; sd = (IADsSecurityDescriptor) entry.Properties["ntSecurityDescriptor"]).Value; Console.WriteLine("owner={0}", sd.Owner); } |
In some cases, we may need to do an explicit RefreshCache to load the ntSecurityDescriptor attribute into the property cache before accessing it.
Using DirectorySearcher
Once again, we refer to the sample in Chapter 5 for a demonstration of using DirectorySearcher for reading a security descriptor.