Directory Object Permissions in Active Directory and ADAM
In Active Directory and ADAM, the permissions for objects in the directory are determined using Windows security descriptors. These are the same security descriptors used throughout the Windows security model for other objects, such as files and registry keys. In fact, Active Directory and ADAM make some of the most extensive use of the features available in the system to provide such capabilities as read and write permissions on individual attributes.
.NET 2.0 Object Security Model
This is another part of SDS that has received significant improvements in .NET 2.0. .NET 1.x lacked built-in support for working with security descriptors on any type of object, be it a file, a registry key, or a directory object. In order to program security descriptors, developers always had to rely on some sort of interop mechanism, whether they used Windows Management Instrumentation (WMI), one of the ADSI wrappers, or a custom P/Invoke wrapper of the Win32 security APIs.
All of this has changed with version 2.0. Microsoft has introduced a new model for managing security descriptors based on types in the new System.Security.AccessControl namespace and on some additions to the System.Security.Principal namespace. Because the new model applies to all objects using security descriptors in Windows, developers can use the same familiar set of classes to set security descriptors for files and registry keys, as well as directory objects.
SDS has also been enhanced to support a new mechanism for reading and writing security descriptors based on a new property on the DirectoryEntry class, called ObjectSecurity (which is also used on objects such as FileStream for consistency). A variety of new types and enumerations that support some of the Active Directory-specific settings for access control list (ACL) entries have been added as well.
The central object in the system is the ActiveDirectorySecurity class, which is essentially an Active Directoryspecific wrapper around the Windows security descriptor structure. This class provides all of the members required to manipulate the discretionary access control list (DACL, for access control), the system access control list (SACL, for auditing), and the other properties of a security descriptor, such as the owner, primary group, and protected status.
A variety of other objects come into play as well.
Access Control Entry Wrappers
An ACL contains a set of access control entries (ACEs). Active Directory and ADAM support some specific types of ACEs, such as the right to create or delete child objects, in addition to more basic functions. In order to make it easier to create these special ACEs, SDS supplies a set of wrapper classes to represent them. Here is a list of the special ACE wrapper classes, with their base class in parentheses:
- ActiveDirectoryAccessRule (ObjectAccessRule)
- ActiveDirectoryAuditRule (ActiveDirectoryAccessRule)
- CreateChildAccessRule (ActiveDirectoryAccessRule)
- DeleteChildAccessRule (ActiveDirectoryAccessRule)
- DeleteTreeAccessRule (ActiveDirectoryAccessRule)
- ExtendedRightAccessRule (ActiveDirectoryAccessRule)
- PropertyAccessRule (ActiveDirectoryAccessRule)
- PropertySetAccessRule (ActiveDirectoryAccessRule)
Having these classes makes it much easier to create and manage the special rights the ACEs represent.
Enumerations
These enumeration values represent Active Directoryspecific versions of the standard enumerations used in security descriptors:
- ActiveDirectoryRights
- ActiveDirectorySecurityInheritance
- PropertyAccess
- SecurityMasks
The most important is the ActiveDirectoryRights enumeration, which represents the Active Directory version of the Access Mask enumerated constant and contains Active Directoryspecific concepts such as "read property" and "write property."
Unlike its ADSI predecessor (IADsSecurityDescriptor), the .NET based access control model allows developers to decide how security principals will be represented. Using an IdentityReference, either a SecurityIdentifier (SID) type or an NTAccount type can be selected at runtime. Since the security descriptor stores SIDs natively, being able to opt out of conversions to NT account format (domainprincipal name) can improve performance dramatically and can even enable some scenarios that were difficult before, such as when the current machine cannot convert the SID into a friendly name, perhaps because it is a workgroup computer.
Reading Security Descriptors
Reading security descriptors is now straightforward. Listing 8.2 shows an example of enumerating the DACL and printing out the owner and group.
Listing 8.2. Listing the DACL
using System; using System.Collections; using System.DirectoryServices; using System.Security.Principal; using System.Security.AccessControl; public class SecurityDescriptors { public static void Main() { DirectoryEntry entry = new DirectoryEntry( "LDAP://dc=mydomain,dc=com", null, null, AuthenticationTypes.Secure ); ActiveDirectorySecurity sec = entry.ObjectSecurity; PrintSD(sec); AuthorizationRuleCollection rules = null; rules = sec.GetAccessRules( true, true, typeof(NTAccount)); foreach (ActiveDirectoryAccessRule rule in rules) { PrintAce(rule); } } public static void PrintAce(ActiveDirectoryAccessRule rule) { Console.WriteLine("=====ACE====="); Console.Write(" Identity: "); Console.WriteLine(rule.IdentityReference.ToString()); Console.Write(" AccessControlType: "); Console.WriteLine(rule.AccessControlType.ToString()); Console.Write(" ActiveDirectoryRights: "); Console.WriteLine( rule.ActiveDirectoryRights.ToString()); Console.Write(" InheritanceType: "); Console.WriteLine(rule.InheritanceType.ToString()); Console.Write(" ObjectType: "); if (rule.ObjectType == Guid.Empty) Console.WriteLine(""); else Console.WriteLine(rule.ObjectType.ToString()); Console.Write(" InheritedObjectType: "); if (rule.InheritedObjectType == Guid.Empty) Console.WriteLine(""); else Console.WriteLine( rule.InheritedObjectType.ToString()); Console.Write(" ObjectFlags: "); Console.WriteLine(rule.ObjectFlags.ToString()); } public static void PrintSD(ActiveDirectorySecurity sd) { Console.WriteLine("=====Security Descriptor====="); Console.Write(" Owner: "); Console.WriteLine(sd.GetOwner(typeof(NTAccount))); Console.Write(" Group: "); Console.WriteLine(sd.GetGroup(typeof(NTAccount))); } } |
This code may look simple, but it is actually one of the most useful tools for security programmers, because the rules for modifying Active Directory and ADAM security are actually quite complicated. We have found that the best approach is to dump out the original security descriptors, use a GUI tool such as ADUC or ADSI Edit to get the settings we want, and then dump out the resulting security descriptors to see what the difference is. Once we can see the differences, it is usually not too hard to get our desired results.
Changing Security Descriptors
In most cases, we hope there is no reason to need to write security descriptors programmatically. This is generally best left to the administrators of the system to perform. In fact, most directory services programming tasks do not involve security descriptor manipulation at all.
However, we might actually need to build a tool for administrators to use to manage security descriptors, perhaps through a web interface in our case. As such, we cannot skip over this step.
Ideally, this book would provide many examples of the different types of security descriptor manipulations we can perform in order to accomplish even the most esoteric Active Directory security task. Unfortunately, that topic could probably fill another book. We will need to suffice with a brief example.
At the most basic level, writing a security descriptor is a fairly simple task. Listing 8.3 shows one such example using the new ActiveDirectory-Security class from version 2.0.
Listing 8.3. Modifying Security Descriptors
DirectoryEntry entry = new DirectoryEntry( "LDAP://CN=some object,DC=mydomain,DC=com", null, null, AuthenticationTypes.Secure ); ActiveDirectorySecurity sec = entry.ObjectSecurity; ActiveDirectoryAccessRule rule = new ActiveDirectoryAccessRule( new NTAccount("mydomain", "super.user"), ActiveDirectoryRights.GenericAll, AccessControlType.Allow ); sec.AddAccessRule(rule); entry.CommitChanges(); |
As Listing 8.3 demonstrates, modifications are straightforward. We simply get the security descriptor for the object in question, make the required modifications, and call CommitChanges, just like with any other modification to a directory object. The hard part is to know what modifications to make to get the desired results!
Interoperability with SDS.P
One of the great things about the .NET security descriptor classes is that they allow for binary and Security Descriptor Description Language (SDDL) import and export of security descriptors. So, instead of using DirectoryEntry and the property cache, we can use other sources, such as DirectorySearcher, or perhaps an LDIF export.
One particularly important integration point is with SDS.P. Since it is a lower-level, generic API, it does not include direct support for security descriptors with a property such as ObjectSecurity. However, it does allow us to read and write the ntSecurityDescriptor attribute on any Active Directory or ADAM object directly as binary. We can then take the resulting byte array and use it to create an ActiveDirectorySecurity object and get binary data in and out of it with the SetSecurityDescriptorBinaryForm and the GetSecurityDescriptorBinaryForm methods.
This allows us to work in our lower-level API for LDAP, but switch to the much more productive .NET-managed ACL for security descriptor work.
Converting between GUIDs and Friendly Names
One of the most difficult tasks in dealing with Active Directory security descriptor esoterica is converting to and from the GUID and friendly name versions of the schema elements and extended rights that are inserted into all of those GUID structures.
Listing 8.4 demonstrates how to convert between the GUID and friendly name of various schema objects and extended rights. It serves as the missing link between the GUIDs used extensively in security descriptors and the way we probably want to work with them. The side benefit is that this sample works well in any version of .NET and we can use it for security descriptor manipulation tasks in .NET 1.x as well.
Listing 8.4. GUID-to-Friendly-Name Conversion
using System; using System.Collections; using System.DirectoryServices; using System.Text; public class SchemaGuidConversion { //shows how to use the class... public static void Main() { DirectoryEntry rootDse; DirectoryEntry schemaRoot; DirectoryEntry extendedRightsRoot; string schemaDN; string extendedRightsDN = "CN=Extended-Rights,"; string schemaAtt = "schemaNamingContext"; string configAtt = "configurationNamingContext"; Guid samGuid = new Guid("3e0abfd0-126a-11d0-a060-00aa006c33ed"); Guid cpGuid = new Guid("ab721a53-1e2f-11d0-9819-00aa0040529b"); rootDse = new DirectoryEntry("LDAP://rootDSE"); schemaDN = (string) rootDse.Properties[schemaAtt].Value; extendedRightsDN += (string) rootDse.Properties[configAtt].Value; schemaRoot = new DirectoryEntry("LDAP://" + schemaDN); extendedRightsRoot = new DirectoryEntry("LDAP://" + extendedRightsDN); Console.WriteLine( "cn={0}", GetSchemaIDGuid("cn", schemaRoot) ); Console.WriteLine( "Validated-SPN={0}", GetRightsGuid("Validated-SPN", extendedRightsRoot) ); Console.WriteLine( "{0}={1}", samGuid.ToString("B"), GetNameForSchemaGuid( samGuid, schemaRoot ) ); Console.WriteLine( "{0}={1}", cpGuid.ToString("B"), GetNameForRightsGuid( cpGuid, extendedRightsRoot ) ); if (rootDse != null) rootDse.Dispose(); if (schemaRoot != null) schemaRoot.Dispose(); Console.ReadLine(); } public static string GetNameForRightsGuid( Guid rightsGuid, DirectoryEntry extendedRightsRoot ) { string filter = String.Format( "(rightsGuid={0})", rightsGuid.ToString("D") ); return GetNameForGuid( filter, "cn", extendedRightsRoot ); } public static string GetNameForSchemaGuid( Guid schemaIDGuid, DirectoryEntry schemaRoot ) { string filter = String.Format( "(schemaIDGUID={0})", BuildFilterOctetString( schemaIDGuid.ToByteArray() ) ); return GetNameForGuid( filter, "ldapDisplayName", schemaRoot ); } public static string GetNameForGuid( string filter, string targetAttribute, DirectoryEntry searchRoot ) { string attributeName = null; SearchResult result; DirectorySearcher searcher = new DirectorySearcher(searchRoot); searcher.SearchScope = SearchScope.OneLevel; searcher.PropertiesToLoad.Add(targetAttribute); searcher.Filter = filter; using (searcher) { result = searcher.FindOne(); if (result != null) { attributeName = (string) result.Properties[targetAttribute][0]; } } return attributeName; } public static Guid GetRightsGuid( string rightsName, DirectoryEntry extendedRightsRoot ) { return GetGuidForName( "cn", rightsName, "rightsGuid", extendedRightsRoot ); } public static Guid GetSchemaIDGuid( string ldapDisplayName, DirectoryEntry schemaRoot ) { return GetGuidForName( "ldapDisplayName", ldapDisplayName, "schemaIDGUID", schemaRoot ); } private static Guid GetGuidForName( string attributeName, string attributeValue, string targetAttribute, DirectoryEntry root ) { Guid targetGuid = Guid.Empty; SearchResult result; object guidValue; DirectorySearcher searcher = new DirectorySearcher(root); searcher.SearchScope = SearchScope.OneLevel; searcher.PropertiesToLoad.Add(targetAttribute); searcher.Filter = String.Format( "({0}={1})", attributeName, attributeValue ); using (searcher) { result = searcher.FindOne(); if (result != null) { guidValue = result.Properties[targetAttribute][0]; if (guidValue is string) targetGuid = new Guid((string) guidValue); else targetGuid = new Guid((byte[]) guidValue); } } return targetGuid; } public static string BuildFilterOctetString( //refer to listing 4.2... } } |
.NET 1.x Interop Model
In .NET 1.x, the story with security descriptors is not nearly as good as it is in .NET 2.0. We do not have managed types or classes for dealing with security descriptors in .NET 1.x, so in order to do the same kind of work, we must use interop. The typical way to do this is to use the ADSI IADsSecurityDescriptor interface, along with its accompanying types for ACLs (IADsAccessControlList) and ACEs (IADsAccessControlEntry). We could also tackle this problem with some third-party libraries that have been developed, but we will focus on the ADSI approach here. All of the following examples will use COM interop and will assume that we have created the appropriate COM interop assembly with tlbimp.exe or by adding a COM reference to activeds.tlb in Visual Studio .NET.
Reading Security Descriptors in .NET 1.x
There is no ObjectSecurity property on DirectoryEntry, as there is in .NET 2.0. However, we can get any attribute value that uses security descriptor syntax (2.5.5.15; see Table 6.1 in Chapter 6) from the property cache. For example:
//given a DirectoryEntry entry bound to an object IADsSecurityDescriptor sec; sec = (IADsSecurityDescriptor) entry.Properties["ntSecurityDescriptor"].Value;
From here, we can access the DACL (and the SACL if we have requested it, as per Chapter 6), as well as the intrinsic properties of the security descriptor itself. Listing 8.5 prints out the trustees and some other values in the DACL.
Listing 8.5. Examining Security Descriptors in .NET 1.x
using ActiveDs; using System; using System.Collections; using System.DirectoryServices; DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=dir,DC=svc,DC=accenture,DC=com", null, null, AuthenticationTypes.Secure ); IADsSecurityDescriptor sd = (IADsSecurityDescriptor) entry.Properties["ntSecurityDescriptor"].Value; IADsAccessControlList dacl= (IADsAccessControlList) sd.DiscretionaryAcl; foreach(IADsAccessControlEntry ace in (IEnumerable) dacl) { Console.WriteLine("Trustee: {0}", ace.Trustee); Console.WriteLine("AccessMask: {0}", ace.AccessMask); Console.WriteLine("Access Type: {0}", ace.AceType); Console.WriteLine("Access Flags: {0}", ace.AceFlags); } |
The enumeration values for the members, such as AccessMask, Ace-Type, and AceFlags, are also contained in the ActiveDs runtime-callable wrapper (RCW) interop assembly. It is important that we use the values defined there, especially for AccessMask, as there are many Active Directory-specific values there. Using the values for filesystem ACLs will get us into trouble!
Notice how we only show the use of DirectoryEntry for these samples? This is because it is somewhat complex to retrieve security descriptors from DirectorySearcher in .NET 1.x. We discussed this in more detail in Chapters 5 and 6.
Note: SID-to-Trustee Name Conversion Issues
One interesting aspect of IADsAccessControlEntry is that it automatically converts the internal SID structure in the ACE to a friendly trustee name. The SID is a key component of the ACE, as it identifies the principal to which the ACE applies. For example, it will convert the SID S-1-5-11 to the friendly NT AUTHORITYAuthenticated Users. This makes the interface easier to use for developers, especially those programming from scripting languages. However, there is a performance penalty for converting SIDs to friendly names, and it requires a security context in which the names can be resolved. This latter point can cause unexpected problems in some situations, as we will see shortly.
Changing Security Descriptors in .NET 1.x
Writing security descriptors in .NET 1.x is very similar in theory to the .NET 2.0 methods. ACEs on the DACL are added, removed, and modified. The DACL is persisted back to the security descriptor, and the security descriptor is written back to the directory (see Listing 8.6).
Listing 8.6. Updating a Security Descriptor in .NET 1.x
//adding a full control ACE to a test container using ActiveDS; using System; using System.Collections; using System.DirectoryServices; DirectoryEntry entry = new DirectoryEntry( "LDAP://CN=testcontainer,DC=mydomain,DC=com", null, null, AuthenticationTypes.Secure ); IADsAccessControlEntry newAce = new AccessControlEntryClass(); IADsSecurityDescriptor sd = (IADsSecurityDescriptor) entry.Properties["ntSecurityDescriptor"].Value; IADsAccessControlList dacl= (IADsAccessControlList) sd.DiscretionaryAcl; newAce.Trustee = @"mydomainsome user"; newAce.AccessMask = -1; //all flags newAce.AceType = 0; //access allowed dacl.AddAce(newAce); sd.DiscretionaryAcl = dacl; entry.Properties["ntSecurityDescriptor"].Value = sd; entry.CommitChanges(); |
Obviously, a better coding practice is to use the correct enumerated types to set the values, but we omitted them for the sake of brevity.
Important Caveats
We mentioned earlier in the chapter that the IADsSecurityDescriptor class converts the SIDs stored in the DACL and SACL into friendly "trustee" names of the form domainprincipal name and that a performance penalty is associated with these conversions. Since the interface is designed for a scripting audience that will generally trade performance for productivity most of the time, this is not usually a problem.
However, the conversion of these SIDs into names and then back into SIDs relies on underlying Windows API functions in the Local Security Authority. These APIs in turn rely on RPC calls and domain trust relationships to perform the resolution. If the machine is not a member of the domain or the current security context is a local machine account, calls to resolve domain SIDs may fail.
These failures usually result in timeouts, which can make the time required to read an IADsSecurityDescriptor several orders of magnitude greater than normalmore than 1 minute in some cases. More important, though, is that the COM object may be left in some kind of corrupted state that cannot be written back to the directory, even if it has not been modified.
This last issue can make security descriptors very difficult to deal with in some scenarios, such as in ASP.NET applications where no domain membership exists for the machine or where a local machine account is being used. In some deployments, it may be impossible to perform security descriptor manipulation with these interfaces.
With ADAM, there are some additional problems. ADAM uses ADAMspecific SIDs that by default do not work with the IADsSecurityDescriptor class, as they do not belong to a domain or to the local machine. Essentially, the underlying APIs that are used to look up SID values do not know to look into ADAM instances to resolve them. This culminates in both poor performance characteristics as well as an inability to modify ADAM security descriptors. Hopefully these issues will be addressed in future versions of ADSI and a fix will be released in a future service pack.
Until a fix exists, our best bet is to use .NET 2.0, as the ActiveDirectorySecurity class does not suffer from this problem. Another option is to use the Process class to invoke the ADAM version of the DSACLs.exe command-line tool, although that is inelegant. Other wrapper classes may also be used (see the next section, Other Approaches).
Other Approaches
Because .NET 1.x did not include any support for security descriptors, a variety of enterprising people implemented libraries to fill this gap. One popular implementation exists as open source on the GotDotNet web site.[6] It is difficult to use from Visual Basic .NET due to its extensive use of unsigned integers, but it is otherwise extremely useful and powerful.
[6] www.gotdotnet.com/Community/UserSamples /Details.aspx?SampleGuid=e6098575-dda0-48b8-9abf-e0705af065d9
The trick is to get the data into and out of the directory in raw binary format to use as input to these APIs. One approach is to use DirectorySearcher, but there are some issues with that, as discussed in Chapter 6. Another approach is to use IADsPropertyList and IADsPropertyValue2 to accomplish this via COM interop with SDS. In fact, this is how .NET 2.0 accomplishes the same thing! A sample called "Raw Data Conversion with IADsPropertyList" is available on this book's web site.