Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

ADSI Hunter

4.79/5 (9 votes)
26 Oct 2010CPOL4 min read 1   1.2K  
Active Directory lookup utility

ADSIHunter.JPG

Yes, this image has been doctored so that the domain names are not visible. When the application starts, it will automatically populate the Domains window with all known (to the system) domains.

Introduction

If you work on a Microsoft network, chances are you're using Active Directory (AD). Active Directory stores information about network resources for a domain. This information requires specific authority for update, but is typically open to authenticated users for query.

I developed this tool to allow for exactly these queries. It provides a list of known (to the network) domains, and allows the user to view groups, group membership, users, and user details without the need to dive into LDAP queries. In short, it's easy to use, quick, and provides more information than the typical user really needs.

This tool was developed using the .NET Framework 2.0 only. There are no interop assemblies or Win32 API calls involved in ADSI operations, there is one Win32 API called for the About box animation. This is a .NET 2.0 Windows Forms application.

Background

Many of the organizations that I work for utilize AD to manage application and resource access by groups. Unfortunately for me (and others), many of these organizations do not permit access to the Microsoft Active Directory tools, so verifying that a particular user has been given membership to a particular group can be a bit of a pain. Hence, this tool was born.

Using the Code

The UI itself is pretty straightforward. Just a typical .NET Windows Forms application. The meat of the application is located in the ADLookup class. This class performs all of the AD activities used to populate the lists in the UI. Perusing the source will provide you with an introduction (possibly a rude one) to the world of AD searches in the .NET environment.

If you look at the image above, the arrows indicate that a selection in a list will trigger the automatic update of the list being pointed to. In addition, if a user is selected from the Users in Group list, that user will be selected in the Domain Users list as well, triggering subsequent updates. Likewise, a selection in the Groups for User list will select that group in the Groups in Domain list, triggering subsequent updates. The numbers in parenthesis above each list indicate how many elements are in the list. This gives an at-a-glance answer to one of the most common AD questions: "How many users are in group xx?"

Searching

To utilize the search, you need to select a search option from the Search menu, or you can right-click on either the Groups in Domain, Users in Group, or Users in Domain lists. When you select one, a pop-up window will display for you to enter your search data. The search data you enter is used as a Regular Expression to evaluate against the data in the selected list, so feel free to use .NET Regular Expressions to perform your fuzzy search.

Only the first match of your search criteria is selected. When it is selected, the appropriate lists will be updated in their content as well.

Points of Interest

There are three methods in the ADLookup class that deserve a little attention here. These three methods are used to decode arrays of bytes that are returned from the AD query in the user properties collection.

First, the easy one - SIDToString:

C#
/// <summary>

/// Convert a binary SID to a string.
/// </summary>
/// <param name="sidBinary">SID to convert.</param>
/// <returns>String representation of a SID.</returns>
private string SIDToString(byte[] sidBinary)
{
    SecurityIdentifier sid = new SecurityIdentifier(sidBinary, 0);
    return sid.ToString();
}

The best part of this method is that there's virtually nothing to converting a Windows SID (security identifier) bit array to a human readable string.

The next one is a Registry lookup used to determine the currently active time bias on the system. This is a value used by the system to convert from Greenwich Mean Time (GMT) to the local time.

C#
/// <summary>
/// Retrieve the current machine ActiveTimeBias.
/// </summary>
/// <returns>an integer representing the ActiveTimeBias in hours.</returns>

private int GetActiveBias()
{
    // Open the TimeZone key
    RegistryKey key = 
      Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet" + 
      @"\Control\TimeZoneInformation");
    if (key == null)
        return 0;

    // Pick up the time bias
    int Bias = (int)key.GetValue("ActiveTimeBias");

    // Close the parent key
    key.Close();

    // return the result adjusted for hours (instead of minutes)
    return (Bias / 60);
}

This value is always subtracted from GMT to arrive at the local time. Where I live, we use daylight savings time as well as standard time, so my ActiveTimeBias value will be either 7 (Pacific Daylight Time [PDT]) or 8 (Pacific Standard Time [PST]).

The last method we will visit here is called DecodeLoginHours. Within the properties collection for a user in AD, there exists the ability to limit the hours that a user can log in to a system. This property consists of an array of 21 bytes, where each bit represents a one hour span beginning with Midnight Sunday GMT. Note that I said GMT. This is where the ActiveTimeBias comes in. By performing the subtraction, we're able to re-align the bit-array to machine time. Obviously, this bit-array is not friendly to humans, so we decode it into something that we can easily read. Within the UI, it will show up in the Properties for User list as Login Hours: > Click to view <. Naturally, the user needs to click the item in the list to get the following display:

LoginHours.JPG

C#
/// <summary>
/// Translate the hours into something readable.
/// </summary>
/// <param name="HoursValue">Hours to convert.</param>
/// <returns>A string indicating the hours of availability.</returns>

private string DecodeLoginHours(byte[] HoursValue)
{
    // See if we have anything
    if (HoursValue.Length < 1)
        return string.Empty;

    // Pick up the time zone bias
    int Bias = GetActiveBias();

    // Convert the HoursValue array into a character array of 1's and 0's.
    // That's a really simple statement for a bit of a convoluted process:
    //  The HoursValue byte array consists of 21 elements (21 bytes) where
    //  each bit represents a specified login hour in Universal Time
    //  Coordinated (UTC). These bits must be reconstructed into an array
    //  that we can display (using 1's and 0's) and associated correctly to
    //  each of the hour increments by using the machines current timezone
    //  information.

    // Load the HoursValue byte array into a BitArray
    //   This little trick also allows us to read through the array from 
    //   left to right, rather than from right to left for each of the 21
    //   elements of the Byte array.
    BitArray ba = new BitArray(HoursValue);

    // This is the adjusted bit array (accounting for the ActiveTimeBias)
    BitArray bt = new BitArray(168);

    // Actual index in target array
    int ai = 0;

    // Copy the source bit array to the target bit array with offset
    for (int i = 0; i < ba.Length; i++)
    {
        // Adjust for the ActiveTimeBias
        ai = i - Bias;
        if (ai < 0)
            ai += 168;

        // Place the value
        bt[ai] = ba[i];
    }

    // Time to construct the output
    int colbump = 0;
    int rowbump = 0;
    int rowcnt = 0;
    StringBuilder resb = new StringBuilder();
    resb.Append("      ------- Hour of the Day -------");
    resb.Append(Environment.NewLine);
    resb.Append("      M-3 3-6 6-9 9-N N-3 3-6 6-9 9-M");
    resb.Append(Environment.NewLine);
    resb.Append(_DayOfWeek[rowcnt]);
    for (int i = 0; i < bt.Length; i++)
    {
        // Put in a 0 or a 1
        resb.Append((bt[i]) ? "1" : "0");
        colbump++;
        rowbump++;

        // After 24 elements are written, start the next line
        if (rowbump == 24)
        {
            // Make sure we're not on the last element
            if (i < (bt.Length - 1))
            {
                rowbump = 0;
                colbump = 0;
                resb.Append(Environment.NewLine);
                rowcnt++;
                resb.Append(_DayOfWeek[rowcnt]);
            }
        }
        else
        {
            // Insert a space after every 3 characters
            // unless we've gone to a new line
            if (colbump == 3)
            {
                resb.Append(" ");
                colbump = 0;
            }
        }
    }

    // Return the result
    return resb.ToString();
}

History

  • Version 1.2.1.0 - Initial release to everyone
  • Version 1.2.1.1 - Fixed decoding of the LoginHours when the TimeBias value is negative

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)