InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

InfiniTec.DirectoryServices now on CodePlex

I’ve just created my second CodePlex project (the first one being my PushNotification Lister on http://exchangenotification.codeplex.com/). It’s the DirectoryServices.Protocols wrapper I created a while ago. The last version was still based on my old InfiniTec.Threading library which is seriously broken. The new one runs rather flawlessly and is very stable.

I will add example programs soon, I hope.


Posted by Henning Krause on Tuesday, August 11, 2009 9:14 PM, last modified on Monday, November 29, 2010 8:40 PM
Permalink | Post RSSRSS comment feed

Another update for the DirectoryServices library

This update fixes some serious bugs and adds the ability to save changes made to an item back to the directory server:

    1 ActiveDirectoryUser user;

    2 

    3 using (Connection connection = newConnection("contoso.local", DirectoryIdentifierType.Server, false, newNetworkCredential("administrator", "password", "contoso"), AuthType.Basic))

    4 {

    5     user = newActiveDirectoryUser("CN=Alice, CN=Users, DC=contoso, DC=local", connection);

    6     user.Refresh();

    7 

    8     user.Description = "The quick brown fox jumps over the lazy dog";

    9 

   10     user.Save(false);

   11 }

The Save and SaveAsync methods both accept one boolean parameter. If set to true, the save operation will return before the changes have been committed to disk by the directory server (known as lazy commit). This improves performance, but increases the likelihood of data loss.

ChangeLog

Changes made from version 1.0 to version 1.1:

  • Added a static Open method to the ActiveDirectoryPrinicipal object, which either returns an ActiveDirectoryUser or an ActiveDirectoryGroup object

  • Added the ability to save changes back to server.

  • Fixed a bug with integrated authentication when using the Translator class

  • Fixed a bug which occured when an ActiveDirectoryEntry was refreshed a second time.

  • Fixed a bug with the ActiveDirectoryGroup class

  • Fixed a bug when an DirectoryOperation threw an exception.

Downloads

Release.zip (117,601 Bytes)
Binaries of version 1.1 signed with the InfiniTec private key
InfiniTec.DirectoryServices.zip (108,984 Bytes)
Source files
Documentation.zip (367,358 Bytes)
Documentation as CHM file.

Technorati:

Posted by Henning Krause on Monday, September 18, 2006 12:00 AM, last modified on Monday, September 18, 2006 12:00 PM
Permalink | Post RSSRSS comment feed

DirectoryServices revisited

As promised in the first article (See here), here is another article on the classes in my DirectoryServices package. Here we go...

Lost in translation...

Ever wanted to translate an accountname like contoso\jdoe to the corresponding distinguished name? The Translator class comes to the rescue:

    1 Translator translator;

    2 TranslationResult result;

    3 

    4 using (Connection connection = newConnection("contoso.local", DirectoryIdentifierType.DnsDomain, false,

    5     newNetworkCredential("administrator", "password", "contoso"), AuthType.Basic))

    6 {

    7     translator = newTranslator(connection);

    8     translator.InputFormat = NameFormat.NT4AccountName;

    9     translator.OutputFormat = NameFormat.DistinguishedName;

   10     translator.Translate("contoso\\administrator");

   11 

   12     result = translator.Results[0];

   13     if (result.Status == TranslationStatus.Success)

   14     {

   15         Console.WriteLine("The " + translator.OutputFormat + " of " + result.InputName + " is " + result.TranslatedName);

   16     }

   17     else

   18     {

   19         Console.WriteLine("Could not translate the name. Error: " + result.Status);

   20     }

   21 }

This class wraps around the DsCrackNames function of the Win32 Directory Services API. Basically, you can translate between these name formats:

  • DistinguishedName
  • NT4AccountName
  • DisplayName
  • UniqueId
  • CanonicalName
  • UserPrincipalName
  • CanonicalNameEx
  • ServicePrincipalName
  • SidOrSidHistory
  • DnsDomainName
  • ListNamingContexts

 

Not every nameformat can be translated to every other. You will get a TranslationStatus.NoMapping error in this case.

The last entry, ListNamingContext, can be used to enumerate all naming contexts in the forest. To use this, set the InputFormat to ListNamingContext. Then, call the Translate method with at least one name (content is completely irrelevant):

    1 Translator translator;

    2 

    3 using (Connection connection = newConnection("contoso.local", DirectoryIdentifierType.Server, false,

    4     newNetworkCredential("administrator", "password", "sub"), AuthType.Basic))

    5 {

    6     translator = newTranslator(connection);

    7     translator.InputFormat = NameFormat.ListNamingContexts;

    8     translator.Translate("any_value");

    9 

   10     foreach (TranslationResult result in translator.Results)

   11     {

   12         Console.WriteLine(result.TranslatedName);

   13     }

   14 }

If the InputFormat is set to NameFormat.Unknown, the directory server tries to determine the format of the name(s) to translate. This causes some performance degration - If you know the format, you should supply it.

Unleashing the power of ambiguous name resolution...

Outlook has a handy feature called ambiguous name resolution: You can type only a part of a name, and Outlook resolves the given name to a complete name, if possible. Active Directory also implements this feature, and it's possible to use it with a special LDAP query: (anr=jo*) will find all items in the Active Directory with a special set of properties (per default, givenName, surname, displayName, legacyExchangeDN, msExchMailNickname, RDN, physicalDeliveryOfficeName, , proxyAddress, sAMAccountName) matches the specified filter.

The PrincipalResolver encapsulates this feature and extends it with an additional feature: If the name being searched for is exactly two characters long, the filter is set to (|(anr=value)(&(givenName=value[0]*)(sn=value[1]*))), which effectively resolves initials. Here an example:

    1 PrincipalResolver resolver;

    2 

    3 using (Connection connection = newConnection("contoso.local", DirectoryIdentifierType.Server, false, newNetworkCredential("administrator", "password", "sub"), AuthType.Basic))

    4 {

    5     resolver = newPrincipalResolver(connection);

    6     resolver.FindAll("al", ResolveTypes.User);

    7 

    8     foreach (ActiveDirectoryUser entry in resolver.SearchResult)

    9     {

   10         Console.WriteLine(entry.DisplayName);

   11     }

   12 }

If the domaincontroller, to which the connection object is bound is a global catalog, the entire forest will be searched. To search only a part of the forest, specify these settings:

    1 resolver.ResolveScope = ResolveScope.Domain;

    2 resolver.SearchRoot = Searcher.RootDomain;

The first line restricts the search to the specified domain, while the second line sets the domain for the search. Two default values are available: Searcher.RootDomain, which searches the root domain of the forest, and Searcher.DefaultDomain, which searches the default domain of the domain controller the current connection is bound to.

The PrincipalSearcher can search either for users, groups or both types. Note, that users do include contacts as well.

Speaking of users and groups

To simplify the handling of users and groups, there are two classes to handle these to types: The ActiveDirectoryUser and the ActiveDirectoryGroup:


(click to enlarge)

The base class for both classes is the ActiveDirectoryEntry, which contains some properties and methods for handling Active Directory entries. Based on the ActiveDirectoryEntry is the ActiveDirectoryPrincipal, which contains some properties regarding group membership and SIDs.

Both, the ActiveDirectoryUser and the ActiveDirectoryGroup inherit from this class: The ActiveDirectoryUser exposes mst the properties available on user objects. The same is true fro the ActiveDirectoryGrup.

For performance reasons, the group memberships are only exposed in SID form (the group memberships are stored in this way). The TranslateSids method can be used to translate those sids to a more readable form.

Thats it for now... I hope this library is of some use to anyone...

Technorati:

Posted by Henning Krause on Sunday, September 17, 2006 12:00 AM, last modified on Sunday, September 17, 2006 12:00 PM
Permalink | Post RSSRSS comment feed

A high-level wrapper around System.DirectoryService.Protocols

Thanks to Joe Kaplan, I spent some time recently playing around with the System.DirectoryServices.Protocols (SDS.P)  namespace. The main advantage of this namespace is control and flexibility: The developer decides when to close a connection. What to search, with which scope. This is possible because the SDS.P classes operate at a much lower level than the DirectoryEntry or the DirectorySearcher class.

Nothing, however, comes without a price. In this case, the price is usability - There is simply no DirectoryEntry in the SDS.P namespace - one has to do a search with a scope of Base. Not quite simple. Additionally, the only datatypes supported on the properties returned from a search operation are byte[] and string. And no support for generics anywhere...

Therefore I created this wrapper around the SDS.P namespace, to make it more usable. Additionally, I included a class to translate names between the various formats used throughout Windows. This class wraps around the DsCrackNames function of the Win32 API, and makes GUID binding trivial - Just translate the security-identifier to the corresponding GUID and bind to the Active Directory object (Yes, I know, the SID can also be used to bind to the Active Directory object - but this is limited to the current domain - one cannot bind to an object outside the current domain).


The Connection class (click to enlarge)
When using this class, you will always start with the connection object. The connection class can either bind to a specific server or to a domain name. Server-less binding is also supported.

If you want to perform your own requests using this class, just call the GetSendRequestOperation() which issues the request aynchronously using my InfiniTec.Threading library. This is not trivial if you are not familiar with the library - if you need advice on this topic, drop me a note and I will post an additional article on this topic.

The code used is something like this:

    1 using (Connection connection = newConnection("entdcsub", DirectoryIdentifierType.Server, false))

    2 {

    3     // Do something interesting here...

    4 }

Binding to specific objects

Once you have connection, you can simply bind to a known object:

    1 using (Connection connection = newConnection("dcaw", DirectoryIdentifierType.Server, false))

    2 {

    3     item = newItem("CN=Doe\, John, CN=Users, DC=AdventureWorks, DC=local", connection);

    4     item.Refresh();

    5     displayName = item.Properties.GetProperty<string>("displayName").Value;

    6 }

It's as simple as this...

The Item class has the following characteristics:


But I don't know the distinguished name of the object....

... don't panic. The Searcher class will help you here. This class is by far the most complex class in this library:


The search operation which will be performed by the Searcher class can be extensively customized. Here are the main option:
Constraints - This property accepts a standard LDAP filter like "(mail=*)" or similar.
IncludeDeletedItems - If true, deleted items are returned.
NamingContextScope - This is important. You can specifiy if you want to search the current naming context only, or search the current and all subordinate contexts.
PageSize - Doing a paged search reduces the resources used during the search. This property let you specify the number of items returned per page.
PropertiesToLoad - Which properties should be populated during the search?
Scope - Do you want to search only the search root, the direct descendents of the searchroot, or all levels below the search root?
SearchRoot - Where does the search begin? If NamingContextScope is set to domain scope, a search root must be specified. Otherwise, this property can be left blank. In this case, the entire forest ist searched.
SizeLimit - This property lets you specify the maximum number of items you want to get. But you should use this sparingly - if more items are returned than specified here, an exception is thrown.
SortKeys - Very handy. This allows server-side sorting of the result set

To start a search, populate the desired fields and call FindAll or FindPage. The first method, performs the search and returns once the search is completed. The FindPage method returns once the next page of items is returned by the server.

For scalability reaonse, you should use the FindAllAsync and FindPageAsync methods - these methods return immediately and thus don't block the current thread. The FindCompleted and ProgressChanged events are fired, whenever an operation completes.

The following example performs an ambiguous name resolution and finds all entries which start with the character a:

    1 staticvoid Main(string[] args)

    2 {

    3     Searcher searcher;

    4     SearchToken token;

    5 

    6 

    7     using (Connection connection = newConnection("dcaw", DirectoryIdentifierType.Server, false))

    8     {

    9         searcher = newSearcher(connection);

   10         searcher.PageSize = 1000;

   11 

   12         searcher.Constraints = "(aNR=a*)";

   13         searcher.NamingContextScope = NamingContextScope.IncludeSubDomains;

   14         searcher.SearchRoot = Constants.WellknownDistinguishedNames.RootDse;

   15         searcher.ProgressChanged += searcher_ProgressChanged;

   16         searcher.FindCompleted += searcher_FindCompleted;

   17 

   18         _Event = newManualResetEvent(false);

   19 

   20         searcher.FindPageAsync();

   21 

   22         _Event.WaitOne();

   23 

   24         Console.ReadLine();

   25     }

   26 }

   27 

   28 staticvoid searcher_FindCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)

   29 {

   30     Console.WriteLine("Finished");

   31     if (e.Error != null) Console.WriteLine("Error: " + e.Error.ToString());

   32     _Event.Set();

   33 }

   34 

   35 staticvoid searcher_ProgressChanged(object sender, SearchProgressChangedEventArgs e)

   36 {

   37     Console.WriteLine("Found items: " + e.Items.Count);

   38     foreach (Item item in e.Items)

   39     {

   40         Console.WriteLine("\t" + item.DistinguishedName);

   41     }

   42     Console.WriteLine();

   43 }

If you want to get one page and continue the search at a later point in time (for example from an ASPX page), you can use the SearchToken class to recreate a search operation:

    1 using (Connection connection = newConnection("dcaw", DirectoryIdentifierType.Server, false))

    2 {

    3     searcher = newSearcher(connection);

    4     searcher.PageSize = 1000;

    5 

    6     // Create a searcher and perform the initial search

    7 

    8     // Now, save the current search token.

    9     token = searcher.SearchToken;

   10 

   11 

   12     // Create a new search operation from the saved token:

   13     searcher = newSearcher(connection, token);

   14     searcher.ProgressChanged += searcher_ProgressChanged;

   15     searcher.FindCompleted += searcher_FindCompleted;

   16 

   17     // Continue the search operation

   18     searcher.FindPageAsync();

   19 }

The SearchToken is marked as serializable, so it can be persisted in the viewstate of an ASPX page.

The other very interestingly looking classes...

There are a number of classes in the library I will discuss in another post... those classes can be used to translate names, or provide a stongly-typed access to properties of ActiveDirectory principals (users, groups), and a search class used to find those principals...

Limitations

I have barely touched the surface of the SDS.P namespace - the current classes are read-only, meaning that changes to the Item class cannot be written back to to directory. This will come with a later release.

License

This library is published as freeware. You may use it in you own programs, commercial or freeware at no cost. You may also modify the classes. All I ask for is that you give credit (a link or something like that) to the InfiniTec website.

Downloads

Documentation.zip (341,126 Bytes)
The documentation, as compiled help file
InfiniTec.DirectoryServices_Release.zip (110,671 Bytes)
Release binaries, signed with the InfiniTec private key
InfiniTec.DirectoryServices_Source.zip (66,783 Bytes)
The source code for this release

Technorati:

Posted by Henning Krause on Sunday, September 3, 2006 12:00 AM, last modified on Monday, November 29, 2010 7:30 PM
Permalink | Post RSSRSS comment feed

Active Directory Helper library

When working with Exchange, it is often necessary to fetch some informations from Active Directory. This helper library has this functionality:

  • Find a user in a multi-domain forest based on his domain\username.
  • Construct the URL for mailbox of a given user, based on his domain\username. This URL can then be used for WebDAV queries.
  • Get a DirectorySearcher for the Global Address list and all the other defined address lists

Full source included, and, believe it or not, documentation :-)

Downloads

InfiniTec Helper Library for Active Directory.zip (52,005 Bytes)
Documentation file as CHM file
ActiveDirectoryHelperBinaries.zip (61,371 Bytes)
Binaries, compiled in release version
source.zip (388,464 Bytes)
Full Source

Technorati:

Posted by Henning Krause on Saturday, August 27, 2005 12:00 AM, last modified on Saturday, August 27, 2005 12:00 AM
Permalink | Post RSSRSS comment feed