Introduction
My employer is developing a large application which makes use of Microsoft Reporting Services. We were informed that Reporting Services security plays nicely with Active Directory (as you might guess it would). So, the idea was brought forward to manage our applications reporting security with Active Directory, but we did not want this to be a manual process. We needed a way to automate this. Our application manages projects. The people in those projects are managed in the project roster. If we could somehow, sync our project roster to Active Directory, then we would have what we were looking for.
So, we needed something that would pull data from our project roster and sync it to Active Directory. So I wrote a component to do just that: Create and manage AD groups.
Background
More than just our app, we thought that various other systems in our organization might want to streamline AD group management. So we wanted to come up with a reusable component that could be used across all of these systems. We wanted this component to be totally decoupled from any data store with which it was synced. This goal resulted in the ADGroupManager
component. The model is pictured with this article. To view the model, right-click and Save As.
The ADGroupManager
component has two main pieces: GroupDefinition
and ADUpdater
. GroupDefinition
provides a way for consuming code to communicate with the library what data needs to be synced. GroupDefiniton
basically just provides an Add Resource method. Whenever a resource is added, GroupDefinition
is responsible for building up groups which mirror how the groups will be in AD. GroupDefinition
has a Group
collection. Groups can contain groups or resources.
Groups will be built up in GroupDefinition
in different ways based on the GroupDefinitionMode
. We wanted some flexibility in the way that we created groups based on the assumption that multiple systems would be making use of our component. Two modes build flat groups based upon one or two tokens. We thought a common application would be Project and Role for the two tokens. The third mode works off of both tokens, but it builds up a parent group to which child groups belong. In our case, a role level group which would contain all project role level groups. All groups are prefixed with a group prefix, which in our case is the Application name.
Once a GroupDefinition
has been built, ADUpdater
can perform the actual sync with AD. It only has one public
method SyncGroups()
.
I have also included the Windows service I wrote to use my component, so you can see what the consuming code looks like. It performs timed operations based on a .NET Timer
. On startup, it reads a config file to get the start and stop times along with the delay between runs. It also contains the connection string and query string for the data store as well as configurable parameters for ADGroupManager
such as the prefix, mode, AD container and path. It also contains a class called Logger
which is basically just a wrapper for .NET EventLog
.
The ADGroupManager
solution contains all of the code for the component as well as all of the unit tests. The PSADGroupWinService
solution contains all of the code for the service (consuming code). I envisioned all of my classes and methods being internal
, except for the public interface
I imagined. I had to change that slightly to accommodate my testing. The RTF doc is a complete documentation of ADGroupManager
. It contains the model diagram in this article as well as information of every method, property, class, member, enum. You name it, it's there.
Using the Code
GroupDefiniton
GroupDefinition
relies on the following to run successfully:
- A mode to determine how groups will be created
- A group prefix which is used as part of the group name
- An LDAP path (not really needed for
GroupDefinition
, but for ADUpdater
) - An LDAP container. The default is Users, but you can specify your own (also for
ADUpdater
)
Once all of these pieces are in place, resources can be added. There are two overloads for this: AddResourceToGroupDefinition(string groupToken, string userId)
and AddResourceToGroupDefinition(string groupToken, string userId, string subGroupToken)
. Based on your GroupDefinitionMode
, only one of these overloads is available to consume code. Calling the wrong overload based on a specific mode will result in a PolicyException
. Also userid must be just the user id and not prefixed with the domain, so DOMAIN\userid is invalid.
Based on the mode, Groups are built up in a flat list or in the case of PrefixExtendedGroupMode
, parent groups are created. Therefore the public
APIs end up delegating to AddResourceToFlatGroup()
and AddResourceViaPrefixExtendedGroupMode()
. Group names are very well defined based on the mode, being either PrefixToken1
or PrefixToken1Token2
with possibly a parent of PrefixToken2
. Therefore, comparison is done based on group name. Resources behave similarly and are only added to child groups. Groups can only exist in AD once, and that paradigm is kept in GroupDefinition
.
ADUpdater
ADUpdater
only needs one thing, a GroupDefinition
. The goal was to create it such that it held everything that was needed to create AD groups. That way it could be simpler. It has only one public
method: SyncGroups()
.
ADUpdater
follows the same convention as the definition by delegating to SyncFlatListGroups()
and SyncExtendedListGroups()
. These methods basically check to see if the group exists in AD, if it does not, it creates the group. With group in place, it adds people to the group. This is done by representing the group and user in AD as a .NET DirectoryEntry
and adding the user to the groups member property. This can be seen in AddResourcesToADGroup()
. Groups similarly are added to other groups. These syncing methods make use of various methods performing individual responsibilities such as Creating a group, Removing people from a group, Checking if the group contains a user, existence of groups, etc.
Definitions must always be complete definitions of the data needing to be synced. Deltas are not applied, because SyncGroups()
has the added responsibility of removing users who no longer belong. All updates are made to a group, and then a commit is performed. Currently, the component makes no provision for removing groups. There are several reasons for this whether it be for no defined way yet to identify groups that need removal to the prospect that groups may be used by other systems and should the group even be deleted.
Groups must have a group type. I have provided an enum
called ADGroupType
which mirrors the available AD group types. The only one I am making use of at the present time is DomainLocal
type. I believe it meets our needs. My understanding is that these types are flags and can be grouped together.
Group and Resource
These are basically just data containers. Group
does have some add/remove methods to manage its collections, but no biggie.
Consuming Code
The consuming code in this article is a Windows service, but could be anything. I tested it quite extensively with a dumb little Windows app with a button and a label. The service reads the following from a config file:
EnableLoging
, True
/False
depending on whether you want to write to event log GroupDefinitionMode
, how groups will be created PrefixName
, the group prefix (our applications name)ADContainer
, the container in AD where groups will be placedADPath
, the root LDAP path where AD resides StartTime
, service will not perform before this time, daily EndTime
, service will not perform after this time, daily DelayInHours,
combined with minutes to provide a delay between syncs DelayInMinutes
, combined with hours to provide a delay between syncs ConnectionString
, to the data store QueryString
, the appropriate data
When the service starts, the config is read and the service is initialized. Configuration must meet valid conditions for service to perform. Property ConfigurationIsValid
assures this. When the delay has passed, the timer goes off, Property IsTimeToExecute
verifies if it is time to execute based on the start and end time and delay. If a sync is currently executing, a secondary sync will not start. Logging is done to the application Event Log. I wrote a wrapper for it called Logger
. I left room to configure it for other event logs, but was satisfied with the application for now.
Please note, as submitted the solution for the service is set to use the Local System account to run the service. You will need to make sure that the service runs under an account that has access to write to ADAM or AD. I found that out the aggravating way.
Points of Interest
I don't know of any network admins that like you to just start creating AD groups, so I recommend that you contact them and see if they can set up a development environment for you. If you cannot get them to do so, try using ADAM instead. I did and it worked well. Unfortunately I had a horrible time trying to get it installed on my machine. Therefore, I have included as part of this article a Microsoft how to for ADAM install (ADAM_Step-by-Step_Guide) as well as a document that I wrote (ADAM How To) after a lengthy support call to Microsoft. ADAM can be downloaded here with a cool AD explorer here.
I don't currently have any plans for updates to the component, but I would look at the possibility of removing old groups, and working on coming up with a display name for groups that might make it easier for end users to identify groups.
I am not an AD expert. I had never written any code against AD before this small component. So if you notice some egregious error, let me know. That being said, the code works.
History
- 29th January, 2008: Initial release of this code version 0.0.1.0