Appendix A. Programming the Directory with the .NET Framework

Why bother learning about the .NET Framework? After all, many AD administrators have been scripting happily for years with VBScript, PowerShell, ADSI, WMI, and a pile of command-line tools. We have been getting along just fine. This .NET stuff is really for the enterprise developers writing line-of-business applications, is it not?

First, let’s be clear that .NET may not be for everyone. There are many techniques available for programming the directory, and you may not need to move away from the tools and techniques you already use. However, there are some compelling reasons to consider it:

  • Powerful features that were once only available to C++ developers are now being exposed in the .NET Framework. This makes it easier for the vast majority of us who do not program in C++ to get to these features.

  • Microsoft has a powerful web development platform called ASP.NET that vastly improves how we can build applications for the Web. Many AD development tasks lend themselves to web-based deployment.

  • Microsoft’s 2008, hardly new! PowerShell command shell, based on the .NET Framework, completely changes the equation for how shell programmers and scripters can program and administer Windows.

A.1. Choosing a .NET Programming Language

.NET is a programming environment that can be accessed from many different languages. In fact, the runtime environment for .NET (or “managed”) code is called the Common Language Runtime, or CLR. The runtime is fundamentally object-oriented, but is otherwise neutral to the language syntax used to program it. At the time of this writing, there are dozens of different languages that can be used to write .NET code, from the time-honored COBOL to experimental languages like F#.

Microsoft itself ships tools for several of these languages, including C#, Visual Basic.NET (or VB.NET), C++, and JScript.NET. It is fair to say that most .NET developers use either C# or VB.NET, with Microsoft itself using C# for the vast majority of its own work. C# as a language looks a bit like C++, but a lot more like Java. However, for many of us, our background is in writing VBScript code, so for this book we will present all of the examples in VB.NET.

This decision will likely make a few people grumpy, but it is the best compromise. For those who are put off by this decision, please take consolation in the fact that the thing that really matters is learning the framework itself. The language details are not usually that important. In fact, the languages are often so similar that there are many free tools that can translate code from one language to another with no loss at all.

Unfortunately, there is no way we can explain the basics of programming .NET or any specific languages feature’s in the space we have remaining. Those sorts of primers already exist on the Internet and in bookstores. Instead, we will take the approach of keeping things simple enough to not require a lot of background and additional depth.

A.2. Choosing a Development Tool

Since the last version of this book was published, the landscape for .NET development tools has expanded as much as the framework itself. Today, you have a lot of choices for how you can write .NET code. The primary decision to make is whether or not you want to use an integrated development environment (IDE) for your development tasks. IDEs simplify development by making it easier to organize your code files and build them into executables.

.NET IDE Options

Back in the days of Visual Studio.NET 2002, Microsoft was basically the only game in town for .NET IDEs. However, that story has changed significantly. Not only does Visual Studio 2012 come in a variety of packages, some of them completely free, but there are other non-Microsoft products available to choose from, such as SharpDevelop from IC# Code.

The point here is that there are many options, and quite a few of them are free. It is not necessary to buy an expensive tool to write .NET code. Although we do not endorse a particular tool, we will say that most .NET developers use Microsoft’s Visual Studio, and the free Express versions are suitable for our needs, so that is a reasonable place to start.

.NET Development Without an IDE

Although an IDE can be nice, we do not actually need one to write code for the .NET Framework. In fact, Microsoft provides a free downloadable SDK with all versions of the framework that includes all the tools we need to build .NET code. Though it is rarely done, it is certainly possible to dive right in with Notepad and a command prompt! There are also a variety of tools available, such as Jeff Key’s Snippet Compiler, that provide an experience similar to scripting, but with compiled code. In addition, all of the examples in this appendix can be accomplished in PowerShell since Windows PowerShell is built on top of the .Net Framework.

A.3. .NET Framework Versions

At the time of this writing, Microsoft has released versions 1.0, 1.1, 2.0, 3.0, 3.5, 4.0, and 4.5 of the .NET Framework. It can be confusing to understand which .NET features are available in which releases, which release came bundled with which operating system release, and how code written for one version will work on another version. Let’s unravel all that.

First, a few general notes on .NET Framework versions:

  • It is not a problem to install multiple versions of the framework on the same machine. They happily coexist when installed side by side. In fact, both .NET 3.0 and 3.5 require .NET 2.0 to be installed, because they are not actually standalone releases. NET 3.5 also requires Service Pack 1 for .NET 2.0 to be installed.

  • By default, code compiled against a specific version of the framework will run against that same version of the framework if it is installed on the machine on which the code is run. However, if that version of the framework is not installed where the code is running but a newer version is installed, the code will run against the newer version.

  • Generally speaking, code written against an earlier version of the framework will run with no problems against a later version of the framework. There are a few backward-compatibility problems, but they are rare overall.

  • Make sure you install the latest service packs as they are released.

Which .NET Framework Comes with Which OS?

Depending on the operating system (OS) of the machine where you want to run .NET code, a version of the framework may already be included. Table A-1 summarizes which OS has what. An article at microsoft.com has an excellent diagram that shows the iterative versions of the framework and what version of the underlying CLR is used.

Table A-1. .NET Framework/Windows OS cross-reference

Operating system

Included framework versions

Windows 2000

None

Windows XP

None

Windows Server 2003

1.1

Windows Server 2003 R2

1.1, 2.0

Windows Vista

2.0, 3.0

Windows Server 2008

2.0, 3.0

Windows 7

2.0, 3.0, 3.5 SP1

Windows Server 2008 R2

2.0, 3.0, 3.5 SP1

Windows 8

4.5

Windows Server 2012

4.5

Note

You can optionally install .NET 3.5 (along with .Net 2.0 and .Net 3.0) on Windows 8 and Windows Server 2012.

Unfortunately, once we include service packs and other factors, the matrix of possible options for .NET Framework installation across all of the different OS versions can get a little hairy. We will not attempt to document this completely and will instead defer to Microsoft’s own deployment guides for the details:

Note

If you’re using .NET 3.5 with a forest that’s at the Windows Server 2008 R2 functional level, make sure you’ve reviewed the hotfix described in Microsoft Knowledge Base article 2260240 to see if it applies to your application.

Directory Programming Features by .NET Framework Release

The .NET Framework includes a huge number of classes and other programming types. In order to keep things organized, these types are arranged in hierarchical groupings called namespaces. The core features of the framework are arranged under the System namespace.

There are now four main namespaces specifically used for directory services programming:

  • System.DirectoryServices (SDS)

  • System.DirectoryServices.ActiveDirectory (SDS.AD)

  • System.DirectoryServices.Protocols (SDS.P)

  • System.DirectoryServices.AccountManagement (SDS.AM)

Note

For brevity, we will refer to the namespaces by the above abbreviations throughout the text.

This does not include all of the other types we might need to access to perform additional programming tasks and also ignores access to WMI (something that .NET can do, but not something we will cover at all in this book).

Assemblies Versus Namespaces

In order to keep the framework modular, major pieces of functionality are compiled into separate components called assemblies. An assembly is essentially just a DLL. An assembly can contain types in multiple namespaces, and namespaces can span different assemblies. Do not let this confuse you, though.

There are three important things to know here:

  • In order to use a type in a specific assembly, our code must reference that assembly.

  • The directory services programming features are bundled into separate assemblies from the rest of the .NET Framework so that code that does not need these features does not need to load the types contained in these assemblies.

  • By default, none of the various projects types in Visual Studio reference any of the directory services programming assemblies, so we must remember to add these references. This will likely apply to whatever tool you end up using for development.

Summary of Namespaces, Assemblies, and Framework Versions

Table A-2 summarizes which namespaces are available in which .NET Framework releases.

Table A-2. Directory services namespaces by framework release

Namespace

Assembly

Version

System.DirectoryServices

System.DirectoryServices.dll

1.0+

System.DirectoryServices.ActiveDirectory

System.DirectoryServices.dll

2.0+

System.DirectoryServices.Protocols

System.DirectoryServices.Protocols.dll

2.0+

System.DirectoryServices.AccountManagement

System.DirectoryServices.AccountManagement.dll

3.5+

The bottom line is that .NET 2.0 contains the majority of the available features, but you need .NET 3.5 to get all of them. Looking at Table A-1, this also implies that you may need to get one of the newer versions of .NET installed where you want to run your code.

Note

Our recommendation is that you use at least .NET 3.5, and we will assume .NET 3.5 unless otherwise noted. Use the latest service packs as well.

A.4. Directory Services Programming Landscape

First, let’s take a high-level view of how all of the various pieces fit together.

As you might imagine, the .NET Framework does not attempt to implement the entire operating system from the ground up, but instead uses existing operating system services to some extent or another and repackages them in the more consistent .NET programming model. All of the .NET directory services features leverage existing operating system functionality, some of which we have already covered in this book.

Figure A-1 depicts how the various pieces fit together.

Windows Directory Services API layers
Figure A-1. Windows Directory Services API layers

As with most software architectures, the model is built up in layers from low-level components to high-level ones. At the top, we see how the various .NET components fit together and depend on each other.

Right at the center of the diagram, we see ADSI and how it depends on various lower-level operating system components. You will find it reassuring that much of the investment you have already made in learning ADSI from this book and other sources will directly carry over to your .NET programming activities.

Now, let’s dig into the high-level details of each namespace in turn.

System.DirectoryServices Overview

The System.DirectoryServices namespace (SDS) is the primary namespace we use for programming directory services in .NET. It is the oldest namespace, having been included with .NET since the very first 1.0 release.

SDS is basically just a simple interoperability layer over the existing ADSI programming model. Most of what you already know about ADSI will apply directly to SDS. SDS is in many ways like a stripped-down version of ADSI, but in other ways it is more powerful. In all cases, it is ADSI that does the bulk of the work, with the underlying components it depends on doing the actual heavy lifting.

Note

By “stripped down,” we mean that SDS supports the core interfaces in ADSI, such as IADs and IADsContainer, but does not directly support any of the so-called “persistent object interfaces,” such as IADsGroup and IADsUser. This does not mean that we cannot use those interfaces, but that we may need to jump through some additional hoops to get to them.

Even though there are quite a few types in the namespace, almost everything in SDS revolves around two core classes:

  • DirectoryEntry

  • DirectorySearcher

DirectoryEntry is essentially a wrapper around the ADSI IADs interface and is most similar to programming with the familiar IADsOpenDSObject.OpenDSObject and GetObject methods. In fact, DirectoryEntry allows us to “drop down” into ADSI whenever we need to by exposing a NativeObject property that provides direct access to the underlying ADSI COM object.

DirectoryEntry is used to connect to the directory, refer to specific objects, read and write their attributes, create new objects, delete objects, and move objects. Everything starts with a DirectoryEntry object.

DirectorySearcher is the more interesting part of SDS. As its name implies, it allows us to search the directory via LDAP queries. In the ADSI scripting world, we use ActiveX Data Objects (ADO) for querying the directory via the ADSI OLE DB provider, while we use GetObject to access specific objects and perform modifications. This approach made it easier for developers accustomed to querying SQL databases to learn LDAP, but the two models were never well integrated, and ADO gets a little clunky when special features need to be accessed. Additionally, many of the powerful advanced LDAP query features supported by Active Directory never got exposed to ADO, so developers using this technology were basically out of luck.

DirectorySearcher takes a different approach. Instead of using a similar pattern and having .NET developers use the classes in System.Data to execute LDAP searches, the designers of the framework gave us a component purpose-built for doing LDAP queries. The main advantages are:

  • It offers tight integration with DirectoryEntry and the other supporting classes in SDS.

  • It works in terms of LDAP-specific search concepts such as search base, query scope, and filters instead of using SQL terminology that may not apply.

  • Special features such as paged searches and timeout settings are cleanly supported via strongly typed properties.

  • All of the advanced Active Directory search features exposed by ADSI are supported.

You’ll see how some of this looks when we show some examples in the upcoming sections.

Warning

The .NET Framework data access features in System.Data still support OLE DB data sources. This means that the OLE DB provider we used with ADO in VB and VBScript can still be used with .NET as well. This may seem like the natural thing to do for programmers coming from that background. Don’t do it!

Besides having all of the same clumsiness issues that ADO always suffered from, it is not as fast and is completely unnecessary now that we have DirectorySearcher.

While we are issuing warnings, VB.NET also supports a GetObject method for backward-compatibility reasons. Don’t use this for accessing Active Directory objects either. Use DirectoryEntry instead.

Under the hood, DirectorySearcher accomplishes its magic by interoperating with a low-level ADSI interface called IDirectorySearch. IDirectorySearch has been in ADSI for a long time, but VB and VBScript developers could not call it directly as it was designed only for C++ developers. The ADSI OLE DB provider we use in VBScript is actually another wrapper around IDirectorySearch that adapts IDirectorySearch to the ADO programming model. With DirectorySearcher, we get a feature designed for LDAP programming and with all the special features exposed.

Other nice things in System.DirectoryServices

The .NET Framework has first-class support for Windows security objects such as security descriptors, DACLs, ACEs, and SIDs. Consequently, SDS follows suit and provides support for the full complement of Active Directory security-descriptor-manipulation functions. The classes that support this are much faster and more powerful than the ADSI-based interfaces we used to program them before, such as IADsSecurityDescriptor.

We usually do not spend much time writing code that manipulates security descriptors, but full support for these features is welcome in the cases when we do.

System.DirectoryServices summary

  • SDS is the primary namespace we use for .NET directory services programming.

  • DirectoryEntry and DirectorySearcher are the two main classes.

  • SDS is based on ADSI, so much of our ADSI knowledge applies directly.

  • SDS is a little stripped-down compared to full ADSI, but it works well in general.

  • DirectorySearcher is a better query interface than what we had before.

System.DirectoryServices.ActiveDirectory Overview

As its name implies, SDS.AD provides support for performing operations on Active Directory specifically. When we say Active Directory, we include both AD Domain Services and AD Lightweight Directory Services (AD LDS).

SDS.AD provides access to functions that previously were difficult or impossible for developers using languages other than C++ to access. The functions can be grouped into the following categories:

  • Active Directory infrastructure (forests, domains, global catalogs, domain controllers, sites, etc.)

  • Replication

  • Trusts

  • Directory schema

SDS.AD accomplishes this goal by using a clever design to marry the ADSI programming model to a set of non-LDAP RPC interfaces with function names like DsGetDCName. As we will see, SDS.AD is integrated tightly with SDS, making it easy to move between the two namespaces as needed.

Why use System.DirectoryServices.ActiveDirectory?

You may be looking at the list from the previous section and thinking that the number of times you’ve needed or wanted to be able to write code to create a trust could be counted on one hand (or less), and you might be right. In fact, much of the functionality exposed by SDS.AD is stuff that most of us will never need to automate. The tasks involving SDS.AD classes are infrequent, and other tools are often more appropriate to accomplish those tasks.

The sweet spot for SDS.AD is in the first point, which is the access to Active Directory infrastructure components. The ability to do things like enumerate domains and domain controllers is helpful and very easy to use here. Additionally, SDS.AD provides access to the full power of the DC locator component of Windows (discussed in Chapter 8) and allows us to access all of its advanced features, such as finding domain controllers in specific sites and forcing rediscovery. This was never available to us at all via ADSI or SDS before.

With this in mind, we will proceed to ignore all of the other, more obscure features of SDS.AD and instead concentrate on the infrastructure features in our upcoming examples.

System.DirectoryServices.ActiveDirectory summary

  • SDS.AD provides many powerful features that were not previously available to most programmers.

  • It is tightly integrated with SDS.

  • Some of the functions supported are a bit obscure, but the infrastructure features are always useful.

System.DirectoryServices.Protocols Overview

Unlike SDS.AD, which layers new capabilities on top of the existing ADSI-based SDS, SDS.P is a completely different animal. Referring back to Figure A-1, we see that SDS.P does not layer on top of ADSI at all, but instead sits directly on top of the core Windows LDAP library, wldap32.dll. We also see that SDS.P does not overlap with SDS at all.

In a nutshell, SDS.P gives us a completely different model for programming LDAP. Instead of using the “object-centric” metaphor of ADSI, where you create programmatic objects that point to specific objects in the directory in order to manipulate them, SDS.P applies the “connection-centric” metaphor inherent in the standards-based LDAP API design. For example, in SDS.P we create a DirectoryConnection object to connect to the underlying directory, and then we perform operations on that connection by sending it messages such as search requests and receiving messages such as search responses in reply.

SDS.P implements just about every feature of the underlying LDAP API; it brings the full power of LDAP to the programmer, including many features not even supported by ADSI and thus not possible in SDS. It also provides the opportunity to get the best possible performance from LDAP in scenarios where performance trumps productivity (i.e., not usually scripting).

The downside of this is that SDS.P is more complicated and demands that you learn all of the intimate details of LDAP and AD programming. Basically, it is provided to support the needs of systems programmers rather than administrators or even normal enterprise developers.

Why use System.DirectoryServices.Protocols?

For most of us, the answer is that we will probably never need to. This namespace is not for us, so we can safely ignore it.

However, there are definitely specific scenarios where SDS.P really is needed or desirable, and we can at least list a few of them:

  • You need access to obscure AD LDAP features not supported by ADSI, such as the phantom root or stats controls.

  • You are programming against a non-Microsoft LDAP directory and ADSI does not mesh well with it.

  • You are writing a highly scalable multithreaded server application and need full support for asynchronous queries and maximum performance (not available with ADSI).

  • You use SSL with your LDAP programming and need to override the SSL certificate verification policy so that certain error conditions that would cause an ADSI connection to fail can be ignored.

  • You need to query a directory using the Directory Services Markup Language (DSML) protocol instead of the LDAP protocol.

At the same time, the difference in complexity between writing AD code in VBScript and LDAP API code in C++ is much larger than the difference between programming SDS and SDS.P in VB.NET. If you are inclined to get your hands dirty and are pretty comfortable with LDAP, feel free to check it out. We will provide one simple sample and leave it at that.

System.DirectoryServices.Protocols summary

  • SDS.P provides a powerful, low-level library for programming LDAP directly (no ADSI).

  • It is not intended for use by typical administrators and enterprise developers; it is intended for systems programmers.

  • In specific situations, it may be the only way to accomplish a particular goal, but most of these scenarios are uncommon.

  • It is probably safe for most of us to ignore it.

System.DirectoryServices.AccountManagement Overview

Since its introduction in NET 1.0, SDS has often been criticized for being too difficult to use for performing common user- and groups-management tasks. It was seen as a step down from the approach provided by ADSI before it. Microsoft did not really do anything to address this shortcoming in .NET 2.0, but the 3.5 release changed this significantly.

In .NET 3.5, Microsoft introduced a new namespace and assembly called SDS.AM. Also called “the principal API,” SDS.AM significantly simplifies the tasks associated with managing directory security principals such as users, computers, and groups.

These are the design goals of SDS.AM:

  • Provide a unified design for managing security principals that works the same across all three of Microsoft’s primary directory platforms: AD Directory Services, AD Lightweight Directory Services, and the local machine Security Accounts Manager (SAM) database.

  • Reduce the need to understand LDAP programming or the underlying details of the store being accessed.

  • Make the design flexible so that it can be extended to provide custom functionality and “drop down” into SDS whenever more power is needed.

Referring back to Figure A-1, we see that SDS.AM layers on top of SDS, with a bit of overlap with SDS.P as well. For the most part, SDS.AM is based on SDS and thus ADSI. In a way, we could say that SDS.AM reintroduces features ADSI always had with the IADsUser, IADsComputer, and IADsGroup interfaces, but in actuality it does ADSI one better. Because SDS.AM is much newer, it allowed the designers to purposefully include support for AD LDS and to take advantage of many .NET programming features that simplify development tasks. We will see this when we dive into the examples.

Referring to Figure A-2, we see that there are just a few primary classes within SDS.AM. On the left, we see a class called Principal with several other classes underneath it. The lines here are significant because they indicate an inheritance relationship. UserPrincipal derives from AuthenticablePrincipal, which derives from Principal, etc. Principal is essentially the center of the universe in SDS.AM. We can also derive our own classes from any of these classes as a way to extend SDS.AM to our own directory customizations.

Primary classes in System.DirectoryServices.AccountManagement
Figure A-2. Primary classes in System.DirectoryServices.AccountManagement

PrincipalContext is a helper class used to create different types of Principal objects. Its primary job is to specify which type of directory we are accessing.

PrincipalSearcher and the classes underneath it are used for finding Principal objects in the directory and work in conjunction with PrincipalContext and Principal.

Why use System.DirectoryServices.AccountManagement?

Unlike SDS.P, SDS.AM really is intended to be used by most of us. It significantly simplifies many common tasks and is much easier to use than any of the other directory services programming namespaces. It provides such a high degree of abstraction that it makes it possible for a programmer to know almost nothing about LDAP, ADSI, or the underlying directory model to accomplish common tasks. It is by far the most productive API we have been given to date. It also contains a comparatively small set of types in the namespace, so it requires little effort to get started.

System.DirectoryServices.AccountManagement summary

  • SDS.AM is a namespace designed specifically for managing security principals in Microsoft directories.

  • It requires less knowledge about LDAP and the underlying directories than any of the other namespaces.

  • It provides a highly productive developer experience.

A.5. .NET Directory Services Programming by Example

Now, let’s switch focus and dive into some code. Unfortunately, there is no possible way we can do much more here than scratch the surface of all of the types of things we can do with .NET directory services programming. This topic could easily fill an entire book (and has a few times already). If you need a deeper reference, The .NET Developer’s Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn (Addison-Wesley) is an excellent option.

Here are the conventions for the code samples:

  • For brevity, we will skip error handling and object cleanup code that we really should be using in most “real” work.

  • Assume that we are using a simple console application type for our samples, but do not let that imply that you can only build console applications. All of these samples may be used in any .NET application type.

  • Assume we have an assembly reference to System.DirectoryServices.dll and have imported namespaces for SDS and SDS.AD via the following block of code:

    Imports System.DirectoryServices
    Imports System.DirectoryServices.ActiveDirectory

Note

Like VBScript and VB6, VB.NET supports a programming technique called “late binding.” Late binding allows us to refer to properties or methods on an object that may not be explicitly available on the current class. This is mostly helpful when working with COM interfaces in .NET.

However, C# does not have such a feature and requires complex “reflection” code to accomplish the same goal. For that reason, we will avoid using any late binding in our samples. SDS provides other ways to accomplish the same tasks anyway, so late binding is not really necessary. You can add the Option Strict and Option Explicit directives in your code to disable late binding if you wish.

Additionally, VB.NET is not case-sensitive but C# is, so we’ll use proper case in all of our examples.

Connecting to the Directory

There are two basic options available for connecting to the directory:

  • Use DirectorySearcher.

  • Use one of the infrastructure classes in SDS.AD.

Let’s look at some examples of each in turn. As we discussed earlier in the book, LDAP directories support a special object called RootDSE that allows the programmer to query the directory for information such as what partitions it contains, so we will start there:

'connect to rootDSE using "serverless binding"
Dim rootDSE As New DirectoryEntry("LDAP://RootDSE")

'this uses the full constructor to do the same thing
Dim rootDSE2 As New DirectoryEntry( _
    "LDAP://RootDSE", _
    Nothing, _
    Nothing, _
    AuthenticationTypes.Secure _
    )

'same as first but specifying a specific domain
Dim rootDSE3 As New DirectoryEntry( _
    "LDAP://mycorp.com/RootDSE")

'now we are using explicit credentials and
'Secure (negotiate) authentication
Dim rootDSE4 As New DirectoryEntry( _
    "LDAP://rootDSE", _
    "[email protected]", _
    "Password1", _
    AuthenticationTypes.Secure _
    )

'bind to LDS rootDSE as an AD LDS user using
'simple bind and SSL to protect the credentials
'on the network
Dim ldsRootDSE As New DirectoryEntry( _
    "LDAP://adldsserver.com/RootDSE", _
    "otheruser@adlds", _
    "Password1", _
    AuthenticationTypes.SecureSocketsLayer _
    )

The first thing to notice is that this looks quite a bit like GetObject or OpenDSObject in ADSI. This is no accident. The same technology is being used and the same rules apply. In .NET, we use a feature called “method overloading” to specify multiple constructors for the same object. This makes it easy to either use the defaults or specify explicit credentials and other options whenever we need to. We also show an example of connecting to AD LDS at the end. Again, the same rules apply, except that we cannot use “serverless binding” with AD LDS and must specify a server name.

In the AD LDS example, we connect using SSL and use LDAP simple bind instead of Windows secure bind.

Note

There is no flag to specify a simple bind. Instead, we supply non-null credentials and specify an AuthenticationType other than Secure. It is a little confusing at first, but it actually works the same way in ADSI. If we still want to do a secure bind with a Windows user and also use SSL, we simply combine the flags, like this:

AuthenticationTypes.Secure Or AuthenticationTypes.SecureSocketsLayer

Note

There are many other flags available, but we simply do not have room to explain them all. They are the same flags available in ADSI in the ADS_AUTHENTICATION_ENUM, just with slightly different names that use .NET standards. Reference microsoft.com for a listing of possible AuthenticationTypes.

Now, let’s read some attributes and connect to the domain root object:

'get the defaultNamingContext for this domain
Dim ncName As string = DirectCast( _
    rootDSE.Properties("defaultNamingContext").Value, _
    String)
Dim ncRoot As New DirectoryEntry("LDAP://" + ncName)

'now, let's use SDS.AD to do the same thing
'in one line of code!
Dim ncRoot2 As DirectoryEntry = _
    Domain.GetCurrentDomain().GetDirectoryEntry()

In this example, we show how to read LDAP attributes from the DirectoryEntry we created previously. Instead of using Get and GetEx like we do in ADSI, .NET implements a PropertyCollection that contains PropertyValueCollection objects. The Value property on PropertyValueCollection is a helper that returns:

  • A null reference (Nothing) if the attribute is not set or not available

  • A single value as Object if the attribute has a single value

  • An array of Object values if the attribute is multivalued

This is handy, so we will use it frequently. Since attributes in AD can be of many different data types and .NET converts them to specific data types the same way that ADSI does, they are returned as Object. This means we must convert them back into the type they really are.

After that, we use the defaultNamingContext attribute value from RootDSE to build another DirectoryEntry, this time pointing to the domain partition root object.

Next, we use an alternate approach to accomplish the same goal in a single line of code using the Domain class. Obviously, this is less work and quite handy. Here are some other examples with the Domain class:

'create a Domain-type DirectoryContext pointing to
'a specific domain with default credentials
Dim context1 As New DirectoryContext( _
    DirectoryContextType.Domain, _
    "othercorpdomain.com" _
    )
'build a Domain object with the context
Dim domain1 As Domain = Domain.GetDomain(context1)
'create a Domain-type DirectoryContext pointing to
'a specific domain with alternate plaintext credentials
Dim context2 As New DirectoryContext( _
    DirectoryContextType.Domain, _
    "othercorpdomain.com", _
    "[email protected]", _
    "Password1" _
    )
Dim domain2 As Domain = Domain.GetDomain(context2)

With SDS.AD, we use an object called DirectoryContext as a way to build all sorts of different types of objects, such as domains, forests, specific servers, and even AD LDS configuration sets or AD application partitions. We see this pattern repeated in SDS.AM with the PrincipalContext class.

Note

We do not set AuthenticationTypes with DirectoryContext in the same way we can when building a DirectoryEntry. The underlying code sets all the right defaults for us, including Signing and Sealing, although there is no option to use either SSL or simple bind. SDS.AD only supports Windows authentication for both AD DS and LDS.

As we said before, SDS.AD provides a vast array of AD management features, and we do not have room here to even scratch the surface. Some of our favorite features include the ability to expand the complete domain tree and locate Global Catalog servers, from which you can directly construct a DirectorySearcher for searching the GC. We’ll end with one final example showing how to enumerate domain controllers in a specific domain, since nearly everyone has to do this from time to time:

Dim currentDomain As Domain = Domain.GetCurrentDomain()
For Each dc As DomainController _
    In currentDomain.FindAllDomainControllers()
    'we can do whatever we want now that we have all the DCs...
    Console.WriteLine(dc.Name)
Next

The example is trivially easy, and that is really the whole point. SDS.AD takes tasks that can range from annoying to nearly impossible and makes them easy to accomplish.

Searching the Directory

One of the primary reasons we have a directory in the first place is to look up information in it, so it stands to reason that searching the directory is one of the things we’ll do most often. Let’s look at some basic examples of searching with DirectorySearcher:

Dim root As DirectoryEntry = _
    Domain.GetCurrentDomain().GetDirectoryEntry()
Dim searcher As New DirectorySearcher(root)
searcher.Filter = "(&(objectCategory=person)(objectClass=user))"
searcher.SearchScope = SearchScope.SubTree

'we only want this attribute returned, so specify it
searcher.PropertiesToLoad.Add("sAMAccountName")

'enable paging by setting a nonzero page size
searcher.PageSize = 1000
Dim src As SearchResultCollection
Using (src)
    src = searcher.FindAll()
    For Each result As SearchResult In src
        Console.WriteLine(result.Properties("sAMAccountName")(0))
    Next
End Using

A DirectorySearcher object always needs a DirectoryEntry to use as a way of establishing the connection to the directory and determining which object will be the base of the search. In this example, we use our previous shortcut with SDS.AD to build a DirectoryEntry pointing to the domain partition root object for this purpose. Note that you can build a DirectorySearcher without specifying this, and SDS will try to do this for you, but it is nearly always better to be explicit about your intent in your code.

We use a standard filter for finding user objects and set the scope to SubTree, even though that is not technically necessary since it is the default. We also add the sAMAccountName attribute to the list of PropertiesToLoad to specify that we want only that attribute returned instead of the default of “all,” as per normal LDAP rules.

Note

From a performance perspective, it is important to supply values to PropertiesToLoad as a normal procedure. If we do not, the directory will return Active Directory default attribute values for each matched object. This results in a significant waste of network bandwidth, especially for searches that return many results. It is also worth noting that if we wish to return operational or constructed attributes such as canonicalName or allowedAttributesEffective, we must specify them in PropertiesToLoad as they are never returned in a default search.

We also set the PageSize to 1000 to enable paged searches. As with other ADSI-based searches, we do not explicitly loop through the pages of results returned by the server under the hood. ADSI does this for us. Generally speaking, it is always a good idea to enable paged searches as we usually want to get all of the results, even if there are more than 1,000. If there are less than 1,000, the paging request does not hurt anything.

To enumerate the results, we use a For Each loop against the SearchResultCollection object returned by the call to FindAll. Note that this approach provides better performance than using a For loop based on the Count property of the SearchResultCollection. This has to do with the fact that in order to get the Count, the entire search must be run under the hood anyway. Using For Each instead fetches the results as they are returned.

Notice that like DirectoryEntry, the SearchResult class also has a Properties property that provides access to the attribute values returned by the search. They behave similarly, except that SearchResult objects are strictly read-only and the ResultPropertyValueCollection does not have a handy Value property like the PropertyValueCollection, so we must access the first value returned by indexing into the array. Make sure that if you access an attribute that might be null, you first check to see if the SearchResult contains that attribute using the Contains method. Otherwise, indexing into the array will result in an error:

If result.Properties.Contains("displayName") Then
    Console.WriteLine(result.Properties("displayName")(0))
End If

Note

The SearchResult class contains a method called GetDirectoryEntry that will return the DirectoryEntry object for any given result using the same security context and connection information supplied to the original DirectoryEntry used to build the SearchRoot for the DirectorySearcher. While it may seem tempting to use this method to access the property cache to read attributes from the search, this is a bad idea from a performance perspective. The reason is that creating a DirectoryEntry object results in at least one and usually two additional searches of the directory to fill the property cache for each object. Since the SearchResult object already contains the results we asked for in our search, it makes no sense to access this data again in a less efficient way. Always use the SearchResult when you need to read attribute values from a search.

The appropriate case to call the GetDirectoryEntry method is when you need to perform modifications on the returned object. SearchResult objects are read-only, so we must have a DirectoryEntry if we need to perform a modification.

DirectorySearcher is also the place where many of the special features in AD LDAP are lurking. For example, DirectorySearcher provides access to things like attribute scope queries, directory synchronization, virtual list views, deleted object searches, and Extended DN Queries. Once again, we are pressed for space in this book, but check out some of the other resources available to find out how to use these advanced features if you find yourself in need of them.

Basics of Modifying the Directory

Now that we have some of the basics covered, let’s turn our attention to modifying the directory. After all, searching is only useful if someone puts some objects in the directory first.

We have a few options when it comes to modifying the directory. SDS allows us to create, modify, or delete just about any type of object in the directory that we can think of, as long as we know how. It is our workhorse for modifications, so we will start with it.

On the other hand, much of an administrator’s life tends to revolve around modifying security principals in the directory, such as users, computers, and groups. As we’ve already learned, the namespace in introduced .NET 3.5, SDS.AM, gives us a simple, powerful way to manipulate those types of objects specifically. In the next section, we will show some samples for performing those tasks, using both namespaces for contrast.

Finally, SDS.AD again provides us with a rich set of features for modifying AD infrastructure objects such as sites, trusts, and the schema (which is almost always a bad idea to modify in code, but this is sometimes necessary). This essentially allows you to build your own versions of the AD Domains and Trusts, Sites and Services, and Schema Management MMC snap-ins, if you so desire. While we won’t look at these types of scenarios in depth, we do want you to know where to look if the need arises.

Warning

You cannot perform modification operations on a read-only domain controller. If you attempt to do so, the DC will issue a write referral to a writable DC. This may or may not work.

The other problem with writes to RODCs is that if the write referral did work, the change would go to a different DC than the DC you were trying to access, so the original RODC would not have the new data until replication completes. This can cause chaos in an application that does not anticipate this.

By default, ADSI asks for writable DCs, so this may or may not actually be a problem. The best solution is to be explicit in your intentions and ask for a writable DC. Fortunately, .NET provides a clean way to do this. The LocatorOptions enumeration now provides a WriteableRequired value that can be specified, so make sure you use the overloaded versions of methods for locating DCs (such as Domain.FindDomainController) that allow you to specify LocatorOptions.WriteableRequired when needed.

In order for ADSI on machines running Windows XP or Windows Server 2003 to contact an RODC, you will need to install the compatibility package available from http://support.microsoft.com/kb/944043.

Basic add example

Let’s get started with a simple example, adding a new OU under the root of the domain:

Dim root As DirectoryEntry = _
    Domain.GetCurrentDomain().GetDirectoryEntry()
Dim newOU As DirectoryEntry = _
    root.Children.Add("OU=New OU", "organizationalUnit")
newOU.Properties("description").Value = "A new OU"
newOU.CommitChanges()

Adding a new object to the directory using DirectoryEntry revolves around using the Add method on the Children property of the DirectoryEntry object that will be the parent of the newly added object. The first parameter of the Add method takes the relative distinguished name (RDN) of the new object. For a default Active Directory forest, this will always be CN=xxx, UID=xxx, OU=xxx, or DC=xxx, as those are the four default RDN attribute IDs. With AD LDS, there is more flexibility. The second parameter indicates the objectClass of the new object.

The second thing to notice is that the Add method returns a new DirectoryEntry object. This object is not yet persisted in the directory, but instead exists in memory until we actually call the CommitChanges method. This allows us to set additional attributes on the object before it is first saved; it is important because some objects define mandatory attributes that must be set on the object at creation time, or optional attributes that can only be set at creation time (schema objects are an example here). Here, we show adding a description attribute to demonstrate how this works, although description is not mandatory and could be added after the fact.

Basic remove examples

Removing objects is simple. As a quick example, let’s remove the OU we just created. We will reuse the same variables we already initialized:

root.Children.Remove(newOU)

Once again, we use the Children property on DirectoryEntry, this time calling its Remove method. The Remove method takes another DirectoryEntry object as its parameter, which indicates the object to be removed. As long as the object passed in is a child of the parent and we have the necessary permissions, the deletion happens immediately.

If we instead want to delete an object and all of its descendants, we must use a slightly different approach. For simplicity, let’s pretend “New OU” from our creation example now has a bunch of child objects, including other containers with children:

newOU.DeleteTree()

Here, we call the DeleteTree method on the object that we wish to delete along with all of its descendants. This is a little different than the previous example, where we removed a single object via the Remove method on the parent object’s Children property.

The overall point here is that we must know which type of deletion we want to perform and use the appropriate technique. In both cases, the deletion is immediate. Hopefully it goes without saying that we must be extremely careful when deleting objects in general!

Moving and renaming objects

In ADSI, we have a multipurpose method called MoveHere that we use for doing object moves, object renames, or both operations at the same time. SDS attempts to simplify this model by providing separate MoveTo and Rename methods instead.

MoveTo provides two overloads. One allows us to simply move the object, while the second overload allows us to move and rename it at the same time. Let’s take a look. In this example, we have two DirectoryEntry objects pointing to OUs in our domain, and we wish to move one OU under the other:

Dim firstOU As New DirectoryEntry( _
    "LDAP://OU=First OU,DC=mycorp,DC=com")
Dim newParent as New DirectoryEntry( _
    "LDAP://OU=Second OU,DC=mycorp,DC=com")

'choose one or the other!!!
'this one moves without renaming
firstOU.MoveTo(newParent)

'this one moves and renames
firstOU.MoveTo(newParent, "OU=Child OU")

In the last line, where we rename the object while moving it, we supply the new name in RDN format as we did in the creation sample. We also use this same approach when using the Rename method to change an object’s name without moving it. Let’s rename our second OU from the previous sample:

Dim toBeRenamed As New DirectoryEntry( _
    "LDAP://OU=Second OU,DC=mycorp,DC=com")
toBeRenamed.Rename("OU=A different name")

Modifying existing objects

The final aspect of modification basics is changing attribute values on existing objects. For the most part, this works exactly the same way we saw in our creation example, where we added an attribute value between the time we initially created the object and when we saved it to the directory. We set the desired attribute to the value we need and call CommitChanges when we are done:

Dim entry as New DirectoryEntry( _
    "LDAP://CN=test,OU=People,DC=mycorp,DC=com")

entry.Properties("displayName").Value = "new name"
entry.Properties("description").Value = "new description"
entry.CommitChanges()

Using the Value property replaces the existing value with the new value or sets the attribute if it was not set previously.

If the attribute is multivalued and we wish to modify incrementally instead of replacing the whole thing, we should instead use the Add and Remove methods to add and remove specific values. We can also replace an entire multivalued attribute using the Value property again. To remove all attribute values, we call the Clear method.

The other main thing to keep in mind is that we must supply data in the appropriate type for the attribute value being set. Most directory attributes are strings, but some take numeric, date, or binary data, so check the schema reference documentation to know for sure.

Managing Users

Now that we have the basics of directory modifications mastered, let’s look at a few user management examples. As we have learned by now, effective user management comes down to knowing many of the picky little details about how data is stored in the directory and how to change it to get the results we need. Let’s start with a simple user creation sample:

Dim parent As DirectoryEntry = New DirectoryEntry( _
    "LDAP://OU=people,DC=mycorp,DC=com")
Dim user As DirectoryEntry = _
    parent.Children.Add("CN=test.user", "user")
user.Properties("sAMAccountName").Value = "test.user"
user.Properties("userPrincipalName").Value = "[email protected]"
user.CommitChanges()

This looks fairly similar to our previous example of creating an OU, except that we set some different attributes for the user object. The problem with this is that this user will not have a password and will be disabled by default, so it is not very useful yet. Additionally, if we have a password policy in place requiring passwords, we cannot enable the user until after we have set a password. Let’s revise our sample:

Dim parent As DirectoryEntry = New DirectoryEntry( _
    "LDAP://OU=people,DC=mycorp,DC=com")
Dim user As DirectoryEntry = _
    parent.Children.Add("CN=test.user", "user")
user.Properties("sAMAccountName").Value = "test.user"
user.Properties("userPrincipalName").Value = "[email protected]"
user.CommitChanges()

'this is how we call an IADsUser method
user.Invoke("SetPassword", New Object() {"Password1"})
'this is how we call an IADsUser property method
user.InvokeSet("AccountDisabled", New Object() {False})
'or we could do this, which is faster:
user.Properties("userAccountControl").Value = 512 'normal

'force password change at next logon
user.Properties("pwdLastSet").Value = 0
user.CommitChanges()

Suddenly, our sample is getting a little complex. First, notice that we use a method on DirectoryEntry called Invoke to call methods on underlying ADSI interfaces. In this case, we call IADsUser.SetPassword. Invoke takes an array of objects as its other argument (although in this case it really just wanted a single string), because the method we could be calling under the hood might take any number of different arguments of any type and we need a generic mechanism to make this work.

Next, we see a call to InvokeSet. While Invoke is used for calling ADSI methods, InvokeGet and InvokeSet are used specifically for calling the set and get versions of ADSI properties—in this case, IADsUser.AccountDisabled. We also show how you could set userAccountControl directly instead of using the ADSI property, although we cheat a little by setting a direct numeric value instead of changing the single bit we need to flip in order to remove the disabled flag. In general, setting this attribute to an arbitrary value is bad practice. Instead, we should manipulate the individual bit flags to avoid overwriting other settings and make our intentions in our code more clear.

Finally, we set the pwdLastSet attribute to 0 to force the user to change her password at the next login and call the CommitChanges method to update the object in the directory.

We also see that we have to do this whole thing in three steps. We cannot call SetPassword on an object that has not been created yet, and we cannot enable an object that has no password, so all three steps are needed.

Now, let’s pretend we need to add a user to AD LDS. In this case, we use a totally different method to enable the user. Instead of using userAccountControl, AD LDS uses the msds-userAccountDisabled Boolean attribute (set to False, as you might guess). While this is certainly easier to deal with than flipping individual bits on userAccountControl, the problem is that we need different code for different stores and have to keep track of all these details.

Once we throw in all of the rest of the user management functions, such as setting the home directory, account expiration, and so on, it gets messy quickly.

Managing users with System.DirectoryServices.AccountManagement

Now, let’s take a look at how SDS.AM attempts to simplify this by creating the same user again:

Dim principalContext As new PrincipalContext( _
    ContextType.Domain, _
    "MyCorp", _
    "OU=People,DC=mycorp,DC=com")
Dim newUser As New UserPrincipal( _
    principalContext, "test.user", "Password1", True)

Obviously, this is much simpler. The fourth parameter tells SDS.AM that we would like the user enabled during the creation. In fact, the only real LDAP thing we need to know here is the distinguished name of the container we want to put the user in.

Where it gets even nicer is the fact that we can create a user in either the local SAM database or AD LDS simply by varying how we build the initial PrincipalContext object. The second line of code stays the same:

Dim samContext As New PrincipalContext(ContextType.Machine)
Dim ldsContext As New PrincipalContext( _
    ContextType.ApplicationDirectory, _
    "localhost:50000", "OU=Users,O=Demo")

Unlike with SDS, where we use the Invoke and InvokeSet methods to call into the underlying ADSI interface members, SDS.AM provides strongly typed .NET properties and methods that allow us to manipulate users, groups, and computers in a friendlier way. For example, we now have methods such as SetPassword, ExpirePasswordNow, IsAccountLockedOut, and UnlockAccount, and properties such as LastPasswordSet and UserCannotChangePassword, to make all of these operations that were once difficult (or at least annoying) both simple and easy to understand.

The productivity gains with SDS.AM do not stop there. SDS.AM also provides:

  • The full set of user and computer provisioning functions, including read, update, move, and delete functions

  • A comprehensive set of group management functions, including provisioning of groups and expansion of group membership for both groups and users

  • A set of functions for finding principals in the directory that require little, if any, LDAP knowledge

  • An extensibility model that allows us to create our own Principal classes that support our own schema modifications or other relevant AD schema that are not directly supported in the “out-of-the-box” Principal classes

  • A high-performance bind authentication feature for performing LDAP authentication in a scalable, dependable way

Again, we can only scratch the surface here. To dig a little deeper, check out the MSDN magazine article “Managing Directory Security Principals in the .NET Framework 3.5” by Ethan Wilansky and Joe Kaplan, from the January 2008 issue.

Overriding SSL Server Certificate Verification with SDS.P

As promised, we have up to this point totally avoided any examples having to do with SDS.P, suggesting that it really is not intended for most programmers and not worth attempting to cover in a high-level introduction such as this one. We also promised one simple sample to show you what it looks like. This sample is also relevant because it is something you cannot do in ADSI at all.

The scenario is that our code needs to connect to an LDAP directory—possibly AD DS or LDS, but also maybe a non-Microsoft LDAP directory—and we need to use SSL. Unfortunately, there is a problem with the server’s SSL certificate. Perhaps the subject name on the certificate does not match the DNS name we must use to access the server, or perhaps we do not trust the certificate’s issuer on our client, or the certificate has expired. ADSI does support SSL/LDAP operations, but it will fail with a “Server Not Operational” error if there is anything wrong with the server’s certificate. This behavior cannot be changed, so with ADSI we are out of luck.

However, the Windows LDAP API does provide more options here (ADSI simply does not expose access to this advanced feature). Since SDS.P provides nearly the entire scope of Windows LDAP functionality, we also have access to this feature in SDS.P and can override the SSL verification logic:

Public Module MyModule
    Sub Main()
        Dim con As New LdapConnection( _
            new LdapDirectoryIdentifier( _
                "badserver.com:636", True, False))
        con.SessionOptions.SecureSocketLayer = True
        con.SessionOptions.VerifyServerCertificate = _
            AddressOf ServerCallback
        con.Credential = New NetworkCredential("", "")
        con.SessionOptions.SecureSocketLayer = True
        con.AuthType = AuthType.Anonymous
        con.Bind()

        'do a RootDSE search and get the currentTime attribute
        Dim search As New SearchRequest( _
            "", _
            "(objectClass=*)", _
            SearchScope.Base, _
            New String(){"currentTime"} _
            )
        Dim response As SearchResponse = _
            DirectCast(con.SendRequest(search), SearchResponse)
        For Each entry As SearchResultEntry In response.Entries
            Console.WriteLine(entry.Attributes("currentTime")(0))
        Next
    End Sub

    Function ServerCallback( _
        ByVal connection As LdapConnection, _
        ByVal certificate As X509Certificate _
        ) As Boolean
    'ignore errors; do not even check the certificate
    'just return true
        Return True
    End Function
End Module

Even though we have tried to scare you with the complexity of SDS.P, the code turns out to be fairly simple.

The main trick here is that we define a method called ServerCallback that implements a method signature of a specific delegate (basically a .NET callback function) defined in SDS.P called VerifyServerCertificateCallback. We tell our LdapConnection to call our callback function during SSL certificate verification by using the AddressOf keyword on the SessionOptions.VerifyServerCertificate member to point to our method. ServerCallback simply returns True, instructing the underlying code to accept the certificate presented by the server and ignore any errors. We could write something more intelligent by looking at the data in the certificate that is passed to us if we wished.

Warning

We are not recommending that you ignore SSL errors as a general practice. This is usually a bad idea, especially in cases where the infrastructure is not something you directly control. SSL is there to help protect us. It is better to try to fix the problem with the server’s certificate instead, but using the correct hostname or adding required trusted root certificates.

The rest of the code just sets up the connection to use SSL and anonymous authentication and connects to the directory using the Bind method. After that, we show a simple RootDSE search, a base scope query with a null search base specified, and return the currentTime attribute to demonstrate a simple search operation. However, the point here is not to demonstrate the search, but to show the SSL verification. We would not even get past the Bind method without our SSL verification override if there was a problem with the server’s certificate.

A.6. Summary

.NET, now in version 4.5, is here to stay, and Microsoft continues to invest in these resources for developers while the options in native code APIs have been largely unchanged for years now. Not only does .NET allow us to do the same things we are used to doing in our other tools, but it also provides access to features that were previously only available to C++ developers and new approaches that make development easier.

.NET is also an essential tool to learn for tackling other new technologies, such as ASP.NET web development and Windows PowerShell.

In this appendix, we took a quick tour of the now-expansive landscape of the directory services development namespace in .NET, including the following:

  • System.DirectoryServices

  • System.DirectoryServices.ActiveDirectory

  • System.DirectoryServices.AccountManagement

  • System.DirectoryServices.Protocols

We’ve only scratched the surface of what we can do, but hopefully we have given you enough to get your feet wet and generate some excitement about the possibilities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset