The finer points of .NET DirectoryServices

The finer points of .NET DirectoryServices

In the past few days I’ve been tasked with writing a .NET service, part of which must do the following:

  • Read all the user accounts from Active Directory.
  • Identify which accounts are disabled.
  • Find out what groups each user belongs to.

Achieving these three simple goals with .NET’s DirectoryServices proved to be surprisingly difficult. Here are workarounds for three common issues you might encounter.

First challenge: Getting more than 1000 results

The first problem I encountered was my DirectorySearcher object only returning 1000 results from FindAll(). I wanted all 6000. This limit was being imposed due to some funny behaviour with DirectorySearcher’s SizeLimit property, which defaults to 1000. If you try to set it higher, it will max out at the server’s limit, which (you guessed it) also defaults to 1000.

The trick is to set the DirectorySearcher’s PageSize value to less than 1000. All the results will be silently paged back from the server in the background, ignoring the SizeLimit.

using (DirectorySearcher searcher = new DirectorySearcher(this.searchRoot)){        // Search for user objects...        searcher.Filter = "(&(objectClass=user)(objectCategory=person))";        // Set the PageSize between 0 and the max page size (1000) to return all        // results at once (invisibly paged on demand in the background). Otherwise,        // a limit of 1000 results is imposed.        searcher.PageSize = 500;        using (SearchResultCollection results = searcher.FindAll())        {                foreach (SearchResult result in results)                {                        DirectoryEntry directoryEntry = result.GetDirectoryEntry();                        // ... process user                }        }}

Second challenge: Identifying disabled accounts

DirectorySearcher and DirectoryEntry have a collection of properties that provide useful information from Active Directory like the sAMAccountName, memberOf list, location and e-mail address.

Unfortunately, no property exists to identify if the account has been disabled. To get around this, we have to use a bitwise OR on the UserAccountControl flags to see if ACCOUNTDISABLE is set.

// Check and see if the ACCOUNTDISABLE flag is set.const int ACCOUNTDISABLE = 0x0002;int flags = (int)directoryEntry.Properties["userAccountControl"].Value;bool isDisabled = Convert.ToBoolean(flags & ACCOUNTDISABLE);

Third challenge: Finding all of a user’s groups

DirectoryEntry has a property called memberOf that, at first glance, looks like an easy-to-use list of all the user’s groups. Unfortunately, under Windows 2000, this collection excludes the user’s primary group.

To get an unabridged list of groups, I used a different method that assembles the account’s tokenGroups (a list of SIDs) into an OR-query, and enumerates the results. Here’s what it looked like:

directoryEntry.RefreshCache(new string[]{"tokenGroups"});// Start building a new LDAP OR query.StringBuilder sb = new StringBuilder();sb.Append("(|");// Attach each tokenGroup's SID to the query.foreach (byte[] sid in directoryEntry.Properties["tokenGroups"])        sb.AppendFormat("(objectSid={0})", BuildOctetString(sid));sb.Append(")");StringCollection groups = new StringCollection();using (DirectorySearcher searcher = new DirectorySearcher(this.searchRoot)){        // Apply a filter from our query, and load the name property of each        // object found.        searcher.Filter = sb.ToString();        searcher.PropertiesToLoad.Add("name");        using (SearchResultCollection results = searcher.FindAll())        {                // Get each group's name, and add it to our StringCollection.                foreach (SearchResult result in results)                        groups.Add(result.Properties["name"][0].ToString());        }}...// Helper function to convert a binary SID into a string format suitable for use// in an LDAP query.static string BuildOctetString(byte[] bytes){        StringBuilder sb = new StringBuilder();                                foreach (byte b in bytes)                sb.AppendFormat("\{0}", b.ToString("X2"));        return sb.ToString();}

Note that with tokenGroups, you might get more groups than expected. It contains all nested security groups, not just the user’s immediate groups.

A bad name for a method

When prototyping new code I often leave a web browser with thesaurus.com open in the background. It may sound pedantic, but I sometimes find it very useful when deciding what name to use for a class, or what verb to use for a method name. Well-versed code is easy to understand; each class’s name defines its role in the application, and each method’s name describes what the function it performs.

One bad example, which has bugged me for a long time, can be found in Visual Studio. Visual Studio can automatically generate a method stub for you to handle an event. These methods are named after the object that raises the event, and the name of the event, with an underscore between them.

For example, to handle the Click event of a button called SaveButton, the following method stub would be generated:

void SaveButton_Click(object sender, EventArgs e){    ...}

This method’s name describes the circumstances under which it gets called, not what it actually does. Methods are supposed to do things. Just because it’s an event handler doesn’t mean the rules no longer apply. Where’s the verb here?

I would propose changing it to generate code that looks like this instead:

void HandleSaveButtonClick(object sender, EventArgs e){    ...}

This example is just as consistent (in fact it actually conforms a lot closer to the .NET naming guidelines), and it describes what the method does — it handles a SaveButton Click event.

St9exception with libstdc++

St9exception with libstdc++

Here’s something I encountered today when writing some C++:

try{    throw std::runtime_error("some message");}catch (std::exception e){    std::cout << "error: " << e.what() << std::endl;}

When run, this code will write “error: St9exception”, instead of “some message” to stdout. “St9exception” comes from libstdc++, in which the default value returned by std::exception::what() is the mangled symbol name. The mistake was that I was catching the exception by value, not by reference. (Too much C# perhaps?)

Instead it should have of course been:

try{    throw std::runtime_error("some message");}catch (const std::exception & e){    std::cout << "error: " << e.what() << std::endl;

Richard Dingwall » Flyweights .NET library

This morning I put the finishing touches to Flyweights, a proof-of-concept .NET memory management library hosted at Google Code. The purpose of the library is simple; if your application stores a number of equal objects that never change, why not just store one object and share a reference to it?

Flyweight<string> a = "Richard";Flyweight<string> b = "Richard";Flyweight<string> c = "Richard";Flyweight<string> d = "Richard";Flyweight<string> e = "Richard";

With Flyweights, only one copy of “Richard” is stored, and each of the objects share a reference to it. Flyweights can be used in a similar manner to Nullable<T>, with a Value property. They are also implicitly castable to and from T.

string sentence = "The quick brown fox jumps over the lazy dog.";Flyweight<string> word = "fox";int n = sentence.IndexOf(word); // implicit cast to string

Documentation, examples and downloads are available from the flyweights project page.

Improved error reporting patch for BNC 2.9.4

Improved error reporting patch for BNC 2.9.4

BNC 2.9.4 is a simple open-source IRC proxy. Their website is down at the moment, but you can see a cached version.

I prefer it over more feature-rich proxies like psyBNC because it’s one-server-per-client model is a natural fit for mIRC‘s multi-server support. It also makes things a lot easier if you prefer to manage channels and servers at the client end (which, in my case, was running 24/7 anyway).

One thing I’m not so impressed by is the lack of proper error reporting. I have written a patch to make BNC return a descriptive error message to the client when a connection attempt fails: instead of “Failed Connection”, you now get “Failed Connection: <errno description>”. This should make debugging connection problems much easier. The patch also fixes a missing header dependency that prevents it from compiling under Mac OS X.

T-SQL equality operator ignores trailing spaces

T-SQL equality operator ignores trailing spaces

Today I discovered something new about SQL Server while debugging an application. T-SQL’s equality operator ignores any trailing spaces when comparing strings. Thus, these two statements are functionally equivalent:

SELECT * FROM Territories WHERE TerritoryDescription = 'Savannah'SELECT * FROM Territories WHERE TerritoryDescription = 'Savannah         '

When executed against the Northwind database included with SQL Server they both return the same row, which has no trailing spaces after its TerritoryDescription.

TerritoryID          TerritoryDescription                               RegionID    -------------------- -------------------------------------------------- ----------- 31406                Savannah                                           4(1 row(s) affected)

This behaviour isn’t immediately obvious from the offset, and isn’t mentioned on the MSDN entry.

To avoid this problem, you should use LIKE instead:

SELECT * FROM Territories WHERE TerritoryDescription LIKE 'Savannah         '

When comparing strings with LIKE all characters are significant, including trailing spaces.

Update: a co-worker discovered yesterday that using LIKE in T-SQL JOINs doesn’t use indices in the same way that the equals operator does. This can have a significant impact on performance. Be warned!

Troubleshooting Windows module dependencies

Troubleshooting Windows module dependencies

I have just got a new computer at work, and over the past week I have been installing all the software that I like to use. One tool I rely on is IISAdmin, a program that sits in the system tray and allows you to run multiple IIS websites under non-server editions of Windows. Unfortunately, when I tried to install it under Window Vista, it failed with the message “Error 1904. Module C:Program Filesiisadminztray.ocx failed to register. HRESULT -2147024770. Contact your support personnel.”

IISAdmin setup error 1904

Attempting to manually register the module with RegSvr32 didn’t work either:

The module "ztray.ocx" failed to load.

I couldn’t find anything searching for either of the error messages, so I did some digging on the module itself. It turns out that ztray.ocx is an old (1997) ActiveX control that allows programs to add an icon to the system tray.

I eventually found Dependency Walker, a tool that scans Windows modules for dependencies. I opened up ztray.ocx to see if it was missing anything.

Viewing ztray.ocx with Dependency Walker

As you can see, ztray.ocx makes calls to a module called msvbvm50.dll, part of the Visual Basic 5.0 run-time. This package is present in Windows XP, but appears to have been dropped from Vista. Luckily it is still available as a free download from Microsoft. Installing it solved the dependency problem, and I was able to install IISAdmin successfully.

Agile: convincing stakeholders

Agile: convincing stakeholders

Yesterday I attended BarCamp Agile Wellington, and although I had to leave early, I thought the event was a great success.

In keeping with the theme of BarCamp I thought I’d share my notes from one of the sessions: an unmoderated group discussion on Agile advocacy in large organisations, and how to convince stakeholders to adopt it.

Problems

  • Unfamiliar jargon.Terms like Agile, Scrum and Extreme Programming can be unfamiliar and scary to those who don’t understand them. What was wrong with regular programming?
  • Agile can’t guarantee certainty. A CIO’s job is to manage risk, and having watertight specifications locked down before projects commence is one way to combat it. Agile may appear riskier by leaving too much room for change, particularly in the public sector.
  • Getting the customer on team. Heavy time commitments and taking a burden of responsibility for the project’s success may not be attractive to customers.
  • Agile is not about playing cowboys or cutting corners. Agile is actually more disciplined than traditional waterfall-style frameworks.

Solutions

  • Hold the jargon, please. Don’t mention Agile, Scrums, Extreme Programming, or the Agile Manifesto by name. They aren’t really that important anyway. Perhaps ‘Agile: Everything but the name’ would be a good mantra to adopt?
  • Agile produces a self-managing team. With greater involvement from team members and customers, the traditional project manager role becomes redundant.
  • Lock it down. Sell a fixed timeframe with a fixed budget, but leave the scope open. Focus on the overall end result of the project rather than individual requirements. This way the size of your project box will be limited, but the blocks that fill it can be swapped and rearranged as you go.
  • Sell it inside out, from the bottom up. Instead of trying to convince CIOs about Agile (who more-likely-than-not will see any change as risk), focus on the people who will actually use it. Change and new ideas are interesting to developers, but scream risk to stakeholders.
  • Don’t tell upper management. Do they even need to be aware about Agile before it can be used? Do they even know what current frameworks their organisations use? Trying to convince them on the benefits of Agile may be a complete waste of time. Sell them on completion of a successful project instead.
  • Just get on with it. Why bother convincing people about Agile at all? The evidence is there, so just get on with it!

BarCamp Agile Wellington

BarCamp Agile Wellington

This Friday I will be attending my first BarCamp here in Wellington. A BarCamp is an “open source” workshop-style event that focuses on user participation and freedom of information.

Mike Riversdale (who I worked with briefly at the Christchurch City Council last year) is running BarCamp Agile Wellington on Friday December 6 at Deloitte House. The theme is Agile and will include seminars on related topics like Scrum and Lean, and how they can be applied to other disciplines such as project management and information architecture.

Mike has assured me that BarCamp’s Fight Club-style “everyone must present” rule will be relaxed, which is lucky for newcomers like me!