Managing Directory Services


Although the “Understanding Domain Trust Relationships” section demonstrates that access to your domain controller information is relatively hard to come by when using a .NET application, working with Active Directory itself is relatively easy. All you need is access to either the DirectoryEntry or DirectorySearcher controls as appropriate. Consequently, you have to manage access to Active Directory using permissions.

The following sections show how to use permissions to manage directory service access in a number of ways. The examples also demonstrate types of Active Directory access. This information is essential because you might need to modify the security for a group or individual user as part of an application. The code in these sections shows how to access individual values and modify them as needed. The example doesn’t modify properties that could make your system inaccessible—I decided not to include this code as an example because the techniques for modifying these dangerous values are the same as the techniques used to modify values with fewer negative consequences.

Using Declarative Active Directory Security

Previous chapters have stressed how you can use declarative and imperative security to secure your code. Generally, the examples have used one form or the other based on the requirements of the application. Active Directory is one place where using a combination of declarative and imperative security is helpful, especially if the program relies on multiple access levels.

The example in this, the “Using Imperative Active Directory Security,” and the “Defining Write Access to Active Directory” sections rely on a combination of imperative and declarative security to verify minimum and required access needs. This section uses the declarative security provided by the DirectoryServicesPermissionAttribute class to establish minimum requirements because Active Directory access is optional for most users. For example, the MainForm class declaration includes the following attribute.

 [DirectoryServicesPermissionAttribute(     SecurityAction.Demand,     PermissionAccess=DirectoryServicesPermissionAccess.Browse)] public class MainForm : System.Windows.Forms.Form 

To use this program, the user must have the right to browse Active Directory. If the user doesn’t have this right, there’s little point in continuing the program. The sole purpose of this program is to look at Active Directory and optionally perform modifications on it.

The DetailForm class also uses this declarative security. However, the user can use this form to write information to Active Directory, so the DirectoryServicesPermissionAccess.Browse permission only signifies one level of access the user could need. In this case, the declarative security acts as a basic filter, rather than an access determination.

Using Imperative Active Directory Security

Ensuring that the user has rights to browse Active Directory is a good first step, but you still don’t know where the user will browse. At this point, you need to use imperative security to ensure that the user doesn’t go beyond the bounds set by the network administrator. Listing 12.2 shows the main browsing form for the example application. This form relies on imperative security to ensure that the user has rights to browse the specified path. This listing isn’t complete, but it does contain the essential elements for this discussion. You’ll find the complete source code for this example in the \Chapter 12\C#\Monitor and \Chapter 12\VB\Monitor folders of the source code located on the Sybex Web site.

Listing 12.2 Accessing Active Directory

start example
private void btnQuery_Click(object sender, System.EventArgs e) {    // Active Directory Permission.    DirectoryServicesPermission                  DSP;    // A single Directory Service Permission.    DirectoryServicesPermissionEntry             []DSPEntry;    // Define a permission entry for the requested path.    DSPEntry = new DirectoryServicesPermissionEntry[1];    DSPEntry[0] = new DirectoryServicesPermissionEntry(       DirectoryServicesPermissionAccess.Browse, txtQuery.Text);    // Set security for the program.    DSP = new DirectoryServicesPermission(DSPEntry);    DSP.Demand();    // Clear the previous query (if any).    lvUsers.Items.Clear();    // Add the path information to the DirectoryEntry object.    ADSIEntry.Path = txtQuery.Text;    // The query might fail, so add some error checking.    try    {       // Process each DirectoryEntry child of the root       // DirectoryEntry object.       foreach (DirectoryEntry Child in ADSIEntry.Children)       {          // Look for user objects, versus group or service objects.          if (Child.SchemaClassName.ToUpper() == "USER")          {             // Fill in the ListView object columns. Note that the             // username is available as part of the DirectoryEntry             // Name property, but that we need to obtain the             // Description using another technique.             ListViewItem lvItem  = new ListViewItem(Child.Name);             lvItem.SubItems.Add(                Child.Properties["Description"].Value.ToString());             lvUsers.Items.Add(lvItem);          }       }    }    catch (System.Runtime.InteropServices.COMException eQuery)    {       MessageBox.Show("Invalid Query\r\nMessage: " +                       eQuery.Message +                       "\r\nSource: " + eQuery.Source,                       "Query Error",                       MessageBoxButtons.OK,                       MessageBoxIcon.Error);    } }
end example

The application begins with the btnQuery_Click() method. At this point, the application knows which Active Directory path to search, so it’s time to verify that the user actually has the required access. The code begins by creating a DirectoryServicesPermissionEntry array. This is the first time that the book uses such a technique. You might think that you actually need a DirectoryServicesPermissionEntryCollection object, but the constructor for the DirectoryServicesPermission class only accepts a DirectoryServicesPermissionEntry array. Size the array to hold all of the permissions you need, but no more. Empty array elements tend to cause problems such as unexplained security errors. Each array element contains a DirectoryServicesPermissionEntry object that consists of an access level and an Active Directory path.

After you create the DirectoryServicesPermission object, you can look at the PermissionEntries property in the debugger to see the DirectoryServicesPermissionEntryCollection. This is the only time that .NET appears to use the collection form of the DirectoryServicesPermissionEntry class.

At this point, application security for the main form is in place. The program has cleared the user to browse a specific Active Directory path. Now the code begins defining the view. The example uses a ListView control to display the output of the query, so the first task is to clear the items in the ListView control. Notice that you specifically clear the items, not the entire control. This prevents corruption of settings such as the list headings.

You can configure all elements of the ADSIEntry (DirectoryEntry) control as part of the design process except the path. The application provides an example path in the txtQuery textbox that you’ll need to change to meet your specific server configuration. You can obtain the correct path from the ADsPath field for the ADSI Viewer application—the application will allow you to copy the path to the clipboard using the Ctrl+C key combination. See the “Using the ADSI Viewer Utility” section for details.

The ADSIEntry.Children property is a collection of DirectoryEntry objects. The application won’t fail with a bad path until you try to access these DirectoryEntry objects, which is why you want to place a portion of the code in a try…catch block. Notice how the code uses a property string as an index into each DirectoryEntry object. Even if the property is a string, you must use the ToString() method or the compiler will complain. This is because .NET languages view each DirectoryEntry value as an object, regardless of object type.

The output of this portion of the code can vary depending on the path string you supply. Figure 12.7 shows the output for a WinNT path. The actual DirectoryEntry values change to match the path type, so using an LDAP path will produce different results. This means you can’t depend on specific DirectoryEntry values within your code, even if you’re working with the same Active Directory entry. The return value issues can lead to security problems if your code design doesn’t handle them. In general, it isn’t safe to assume much about the format of the data coming from Active Directory.

click to expand
Figure 12.7: The WinNT path tends to produce easy-to-read DirectoryEntry values.

Defining Write Access to Active Directory

This example lends itself to demonstrating one security principle that you won’t run into often, but will need to handle. Generally, you can provide protection for an application by hiding menus or simply not making the information available. Many of the applications in this book use that approach to keeping data secure—the best way to keep a secret is not to tell anyone. However, in this example, the code must allow everyone to see the information, but must prevent some users from changing it. Hiding is no longer an option.

Most of the activity for the details form occurs in the constructor. The constructor accepts the username, description, and path as inputs so it can create a detailed query for specific user information. Listing 12.3 shows the constructor code for this part of the example. This listing isn’t complete, but it does contain the essential elements for this discussion. You’ll find the complete source code for this example in the \Chapter 12\C#\Monitor and \Chapter 12\VB\Monitor folders of the source code located on the Sybex Web site.

Listing 12.3 The Details Form Displays Individual User Information

start example
public DetailForm(string UserName, string Description, string Path) {    // The current domain.    AppDomain                        MyDomain;    // The current user information.    WindowsPrincipal                 WP;    // Active Directory Permission.    DirectoryServicesPermission      DSP;    // A single Directory Service Permission.    DirectoryServicesPermissionEntry []DSPEntry;    // Path to the user object.    string                           UserPath;    // LDAP provides more information.    bool                             IsLDAP;    // Required for Windows Form Designer support    InitializeComponent();    // Set the principal policy for this application.    MyDomain = Thread.GetDomain();    MyDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);    // Obtain the current user information.    WP = (WindowsPrincipal)Thread.CurrentPrincipal;    // Detect the user’s role.    if (WP.IsInRole(WindowsBuiltInRole.Administrator))    {       // Define permission entries for the requested path.       DSPEntry = new DirectoryServicesPermissionEntry[1];       DSPEntry[0] = new DirectoryServicesPermissionEntry(          DirectoryServicesPermissionAccess.Write, Path);       // Set security for the program.       DSP = new DirectoryServicesPermission(DSPEntry);       DSP.Demand();    }    else    {       // Make the user note information read-only.       txtNotes.ReadOnly = true;       // Get rid of the Update button.       btnUpdate.Enabled = false;       btnUpdate.Visible = false;    }    // Set the username and description.    lblUserName.Text = "User Name: " + UserName;    lblDescription.Text = "Description: " + Description;    // Determine the path type and create a path variable.    if (Path.Substring(0, 4) == "LDAP")    {       IsLDAP = true;       // LDAP requires some work to manipulate the path       // string.       int CNPosit = Path.IndexOf("CN");       UserPath = Path.Substring(0, CNPosit) +                  UserName + "," +                  Path.Substring(CNPosit, Path.Length - CNPosit);    }    else    {       IsLDAP = false;       // A WinNT path requires simple concatenation.       UserPath = Path + "/" + UserName;    }    // Set the ADSIUserEntry Path and get user details.    ADSIUserEntry.Path = UserPath;    ADSIUserEntry.RefreshCache();    // This information is only available using LDAP    if (IsLDAP)    {       // Get the user’s title.       if (ADSIUserEntry.Properties["Title"].Value == null)          lblTitleDept.Text = "Title (Department): No Title";       else          lblTitleDept.Text = "Title (Department): " +             ADSIUserEntry.Properties["Title"].Value.ToString();       // Get the user’s department.       if (ADSIUserEntry.Properties["Department"].Value == null)          lblTitleDept.Text = lblTitleDept.Text + " (No Department)";       else          lblTitleDept.Text = lblTitleDept.Text + " (" +             ADSIUserEntry.Properties["Department"].Value.ToString()             + ")";    }    // This information is common to both WinNT and LDAP, but uses    // slightly different names.    if (IsLDAP)    {       if (ADSIUserEntry.Properties["lastLogon"].Value == null)          lblLogOn.Text = "Last Logon: Never Logged On";       else       {          LargeInteger         Ticks;      // COM Time in Ticks.          long                 ConvTicks;  // Converted Time in Ticks.          PropertyCollection   LogOnTime;  // Logon Property Collection.          // Create a property collection.          LogOnTime = ADSIUserEntry.Properties;          // Obtain the LastLogon property value.          Ticks = (LargeInteger)LogOnTime["lastLogon"][0];          // Convert the System.__ComObject value to a managed          // value.          ConvTicks = (((long)(Ticks.HighPart) << 32) +                      (long) Ticks.LowPart);          // Release the COM ticks value.          Marshal.ReleaseComObject(Ticks);          // Display the value.          lblLogOn.Text = "Last Logon: " +             DateTime.FromFileTime(ConvTicks).ToString();       }    }    else    {       if (ADSIUserEntry.Properties["LastLogin"].Value == null)          lblLogOn.Text = "Last Logon: Never Logged On";       else          lblLogOn.Text = "Last Logon: " +             ADSIUserEntry.Properties["LastLogin"].Value.ToString();    }    // In a few cases, WinNT and LDAP use the same property names.    if (ADSIUserEntry.Properties["HomeDirectory"].Value == null)       lblHomeDirectory.Text = "Home Directory: None";    else       lblHomeDirectory.Text = "Home Directory: " +          ADSIUserEntry.Properties["HomeDirectory"].Value.ToString();    // Get the text for the user notes. Works only for LDAP.    if (IsLDAP)    {       if (ADSIUserEntry.Properties["Info"].Value != null)          txtNotes.Text =             ADSIUserEntry.Properties["Info"].Value.ToString();       // Enable the Update button.       btnUpdate.Visible = true;    }    else    {       txtNotes.Text = "Note Feature Not Available with WinNT";    } }
end example

The code begins by obtaining the WindowsPrincipal object for the current user. Remember that you must use the SetPrincipalPolicy() method to configure the thread information first. Once the code obtains the WindowsPrincipal object, it detects the user’s current role.

If the user is an administrator, the code adds a new DirectoryServicesPermissionEntry permission using the same technique as for the main form. You might wonder why the code doesn’t also set the DirectoryServicesPermissionAccess.Browse permission. The details form inherits this permission from the main form, so you don’t need to set it again. In fact, if you try to set it, CLR will raise an error. However, this fact brings up an important issue—permissions flow from form to form, so you need to add or revoke permissions as appropriate.

Any user who isn’t an administrator doesn’t have permission to write to Active Directory. Even if you don’t do anything else at this point, the directory entries are safe. However, the example makes txtNotes read-only and removes btnUpdate to keep confusion to a minimum. In addition, this form of data hiding is important because otherwise the user would know that an update option exists.

The process for adding the path to the DirectoryEntry control, ADSIUserEntry, is the same as for the main form. In this case, the control is activated using the RefreshCache() method. Calling RefreshCache() ensures that the local control contains the property values for the user in question.

LDAP does provide access to a lot more properties than WinNT. The example shows just two of the additional properties in the form of the user’s title and department name. While WinNT provides access to a mere 25 properties, you’ll find that LDAP provides access to 56 or more. Notice that each property access relies on checks for null values. Active Directory uses null values when a property doesn’t have a value, rather than set it to a default value such as 0 or an empty string.

WinNT and LDAP do have some overlap in the property values they provide. In some cases, the properties don’t have precisely the same name, so you need to extract the property value depending on the type of path used to access the directory entry. Both WinNT and LDAP provide access to the user’s last logon, but WinNT uses LastLogin, while LDAP uses lastLogon.

WinNT normally provides an easy method for accessing data values that CLR can understand. In the case of the lastLogon property, LDAP presents some challenges. This is one case when you need to use the COM access method. Notice that the lastLogon property requires use of a LargeInteger (defined in the ACTIVEDS.TLB file). If you view the property value returned by the lastLogon property, you’ll see that it’s of the System.__ComObject type. This type always indicates that CLR couldn’t understand the value returned by COM. Notice that the code converts the COM value to a managed type, then releases the COM object using Marshal.ReleaseComObject(). If you don’t release the object, your application will have a memory leak—so memory allocation problems aren’t quite solved in .NET, they just don’t occur when using managed types. The final part of the conversion process is to change the number of ticks into a formatted string using the DateTime.FromFileTime() method.

As previously mentioned, the sample application shows how to present and edit one of the user properties. The Info property is only available when working with LDAP, so the code only accesses the property if you’re using an LDAP path. The code also enables an Update button when using an LDAP path so you can update the value in Active Directory. Here’s the simple code for sending a change to Active Directory.

private void btnUpdate_Click(object sender, System.EventArgs e) {    // Place the new value in the correct property.    ADSIUserEntry.Properties["info"][0] = txtNotes.Text;    // Update the property.    ADSIUserEntry.CommitChanges(); } 

The application uses a double index when accessing the property to ensure that the updated text from txtNotes appears in the right place. All you need to do to make the change permanent is call CommitChanges(). Note that the change will only take place if the user has sufficient rights to make it. In most cases, COM will ignore any update errors, so you won’t know the change took place unless you actually check the entry. Figure 12.8 shows the LDAP output for the sample application.

click to expand
Figure 12.8: The LDAP output is more complete than the WinNT output, but requires more work as well.




.Net Development Security Solutions
.NET Development Security Solutions
ISBN: 0782142664
EAN: 2147483647
Year: 2003
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net