![]() |
Web Development »
ASP.NET »
General
Intermediate
License: The Code Project Open License (CPOL)
Active Directory Roles ProviderBy Daniel_PSAn active directory roles provider |
C#, Windows, .NET, ASP.NET, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
I've written this code in order to fill a percieved gap in Microsoft's current ASP.NET role management offerings. (If you are not familiar with ASP.NET Membership and Role Management, you may want to check out the current framework documentation on Securing ASP.NET Web Sites before continuing this article. The Membership and Role summary pages should give you a good start.)
Currently, if you are using forms-based authentication with Microsoft's ActiveDirectoryMembershipProvider, you are a bit limited in your options for a role provider. There is nothing built into the framework that is as relatively simple to configure and maintain ActiveDirectoryMembershipProvider. The best solution you are left with is setting up AzMan. However, there's a significant learning curve and a lot of conflicting information out there regarding it, and it also requires setup and configuration outside of the web site's web.config.
That's where ADRoleProvider comes into play. Simply stated, ADRoleProvider is a role provider class that allows you to use your existing Active Directory groups as ASP.NET Roles, and attempts to provide that framework as easily as ActiveDirectoryMembershipProvider provides your membership framework. Since it inherits from Microsoft's base RoleProvider class, it should function seamlessly in your site and work with all the standard .NET controls (LoginView, etc) and Web.config authorization settings.
Please make certain that you read through this documentation and understand the security and performance concerns mentioned. Also, look through the code to gain an understanding of what exactly how it operates.
In order to make use of this RoleProvider class, there are only two requirements
System.DirectoryServices.AccountManagement namespace to enumerate group membership.Using the source code that Microsoft has released for SqlRoleProvider as a jumping-off point, I've tried to make this class adhere as close as possible to the behavior of the "built-in" role providers. The only significant difference to note is that this is a read-only provider. Since management should be done only through Active Directory, any role operations that would require write access will throw a NotSupportedException.
IMPORTANT - All of the Active Directory queries in the class are currently made against the LDAP sAMAccountName attribute. Consequently, any time you are referring to a group or user name in the web.config, please be certain you are using the sAMAccountName. However, ActiveDirectoryMembershipProvider can be configured to use either sAMAccountName OR userPrincipalName. To avoid any confusion, the safe bet is to set attributeMapUsername="sAMAccountName" in the membership configuration section of the web.config. The one critical change I am currently planning for this code is the option to use UPN to avoid any confusion. This change should complete and tested within a week. If you are working in a live environment in which users are accustomed to logging in as username@domain.com rather than username, please hold off using this until then.
Certain built-in or common Active Directory groups are specifically excluded in the source code. For example, Exchange Enterprise Servers would never, ever be used as a role, so that is in an internal exclusion list. These system groups cannot be queried against, and will never show up in any results. You can also specify your own groups to ignore in the Web.config (see configuration below).
All of the heavy lifting done by this class involves querying against Active Directory, and there are as many ways to query Active Directory as there are to shoot yourself in the foot. Below is the complete code for retrieving a user's role (i.e. AD Group) membership. The SQL Caching code is ellipsed out for explanation purposes.
/// <summary>
/// Retrieve listing of all roles to which a specified user belongs.
/// </summary>
/// <param name="username"></param>
/// <returns>String array of roles</returns>
public override String[] GetRolesForUser(String username)
{
...
//Create an ArrayList to store our resultant list of groups.
ArrayList results = new ArrayList();
//PrincipalContext encapsulates the server or domain against which all
//operations are performed.
using (PrincipalContext context = new PrincipalContext(ContextType.Domain,
null, _DomainDN))
{
try
{
//Create a referance to the user account we are querying
//against.
UserPrincipal p = UserPrincipal.FindByIdentity(context,
IdentityType.SamAccountName, username);
//Get the user's security groups. This is necessary to
//return nested groups, but will NOT return distribution groups.
var groups = p.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
if (!_GroupsToIgnore.Contains(group.SamAccountName))
{
if (_IsAdditiveGroupMode)
{
if (
_GroupsToUse.Contains(
group.SamAccountName))
{
results.Add(
group.SamAccountName);
}
}
else
{
results.Add(group.SamAccountName);
}
}
}
}
catch (Exception ex)
{
throw new ProviderException(
"Unable to query Active Directory.", ex);
}
}
...
return results.ToArray(typeof(String)) as String[];
}
Do not let the nested if statements throw you off; they are there only to make certain that the proper groups are ignored/used, depending on your configuration, explained below. As the comments explain, programming against System.DirectoryServices.AccountManagement is actually quite simple.
Querying against Active Directory can be pretty slow. To alleviate this, ADRoleProvider includes options to enable caching the results from its Active Directory queries to a Microsoft SQL Server. It seems counterintuitive, but caching querying against SQL Server is generally faster than executing AD queries.
The manner of caching is extremely simple. I vacillated between using a more fully normalized series of tables and the simplified method I finally settled upon. Data is cached to a single table as specified below.
CacheId ApplicationId CacheType CacheKey CacheValue ExpireDT
49 MyApp L AllRoles Customer Service,IT 8/18/2008 2:49:50 PM
50 MyApp U asmith IT 8/18/2008 2:49:50 PM
51 MyApp R IT dsmith,msmith,asmith,jsmith,ssmith,bsmith 8/18/2008 2:49:50 PM
There are certain situations in which this simplified method of caching could present minor annoyances. For example: The cached results for IT is set to expire at 2:50, the results for asmith are set to expire at 3:50, and asmith is removed from the IT Active Directory group. In this situation, there could be an hour period where enumerating IT shows that asmith is not a member, but asmith still thinks he is. This could be avoided by having a group table, a user table, and a join table. However, I decided that the added complexity and overhead was not worth the trade-off, since every time a cache item is set, the integrity of every item involved would have to be verified.
Included in the code zip file is a SQL file that will create the necessary table and stored procedures to implement caching. This has been tested under SQL Server 2005 Standard and Developer editions, and SQL Server 2008 Standard.
Honestly, the impact of enabling SQL caching is much larger than I had thought it would be. My test environment consists of only two groups and about a half dozen users. With such a small set of data to work with, I figured the performance impact would be minimal. The SQL instance I am connecting to is on a separate server from the test site. The code below basically what I used to test performance, though I actually set it to run a few hundred times.
DateTime dtStart;
DateTime dtEnd;
TimeSpan executionTime;
string[] results;
dtStart = DateTime.Now;
results = Roles.GetAllRoles();
results = Roles.GetRolesForUser("dsoref");
results = Roles.GetUsersInRole("IT");
dtEnd = DateTime.Now;
executionTime = dtEnd - dtStart;
tests.InnerHtml += "Execution Time: " + executionTime.TotalMilliseconds + "<br /><br />";
Without caching enabled, this chunk of code would take from 78 to 154ms to execute, with the average hovering at about 98ms.
With caching enabled, the initial request would take from 81 to 204ms to execute, with the average hovering around 102ms since the data needed to be cached to the SQL database. However, subsequent requests within the expiration time took only 14 to 16ms. I guess SQL is faster than AD after all.
First, you will want to compile the provider and copy the resulting .dll into your website's bin folder. You could instead copy the .cs file into your App_Code directory, but this is somewhat less secure. Granted, you should be able to trust all of your developers, but better safe than sorry.
Next, you will want to enter the proper configuration settings into your web.config, as below. I will run through each of these settings.
...
<connectionStrings>
...
<add name="ActiveDirCS"
connectionString="LDAP://DC=YourDomain,DC=com"/>
</connectionStrings>
...
<roleManager enabled="true" defaultProvider="ActiveDirRP">
<providers>
<clear/>
<add applicationName="MyApp"
name="ActiveDirRP"
type="DanielPS.Roles.ADRoleProvider"
activeDirectoryConnectionString="ActiveDirCS"
groupMode="Additive"
groupsToUse="IT, Customer Service"
groupsToIgnore="Senior Management"
usersToIgnore="asmith, ksose"
enableSqlCache="True"
sqlConnectionString="SQLCacheCS"
cacheTimeInMinutes="30" />
</providers>
</roleManager>
...
DanielPS.Roles.ADRoleProvider, or something else if you don't like the namespace.LDAP://DC=YourDomain,DC=com. If you specify a server, an error will be thrown.GetAllRoles() call. All groups you wish to use as roles must be specified in groupsToUse Roles.GetRolesForUser() will never include "Senior Management" in the results, and Roles.GetUsersInRole("Senior Management") will throw a ProviderException. Note: If a groupMode is additive, and a group is specified in both groupsToUse and groupsToIgnore, groupsToIgnore will take precedence.
Note: If a group is specified in groupsToUse, but does not exist in Active Directory, it will be ignored.
The following groups are excluded in the source code. Even specifying them in groupsToUse will not make them functional.
Domain Guests, Domain Computers, Group Policy Creator Owners, Guests, Users, Domain Users, Pre-Windows 2000 Compatible Access, Exchange Domain Servers, Schema Admins, Enterprise Admins, Domain Admins, Cert Publishers, Backup Operators, Account Operators, Server Operators, Print Operators, Replicator, Domain Controllers, WINS Users, DnsAdmins, DnsUpdateProxy, DHCP Users, DHCP Administrators, Exchange Services, Exchange Enterprise Servers, Remote Desktop Users, Network Configuration Operators, Incoming Forest Trust Builders, Performance Monitor Users, Performance Log Users, Windows Authorization Access Group, Terminal Server License Servers, Distributed COM Users, Administrators, Everybody, RAS and IAS Servers, MTS Trusted Impersonators, MTS Impersonators
The following users are excluded in the source code. They will not show up in any group membership enumeration.
Administrator, TsInternetUser, Guest, krbtgt, Replicate, SERVICE, SMSService
System.DirectoryServices.AccountManagement (.NET 3.5 only) and support recursive membership
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 3 Oct 2008 Editor: Sean Ewington |
Copyright 2008 by Daniel_PS Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |