Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 12

Unlike the adsedit tool, the provider automatically set the msDS-UserAccountDisabled attribute to false for you. Of course, if you call CreateUser with the isApproved parameter set to false, then the msDS-UserAccountDisabled field will be set to true by the provider.

With the new user created, you can now try logging in using the Login control. Just type in the username testuser@corsair.com, and you will be logged in successfully. At this point, you can call any of the other methods on ActiveDirectoryMembershipProvider. Fetching the MembershipUser object and displaying its information works as expected. If you enable searching you can call the search related methods as well. If you extend the schema in ADAM with the five attributes necessary for self-service password resets, you can use the ResetPassword method. Overall, you will see that after you get past the ADAM-specific configuration work and unique aspects of connecting to ADAM, ActiveDirectoryMembershipProvider works the same way against ADAM as it does against AD. There is no difference in terms of supported provider functionality between the two directory stores.

Using the Provider in Par tial Trust

All the examples shown so far for Active Directory and for ADAM have been running in full trust. However, if you attempt to use the provider directly in a partial trust environment it will fail. Within the provider’s Initialize method, an explicit check is made for Low trust. The provider itself is attributed with a link demand for System.DirectoryServices.DirectoryServicesPermission. Also, each of its public methods is attributed with a full demand for the same permission.

[DirectoryServicesPermission(SecurityAction.LinkDemand, Unrestricted=true)]

[DirectoryServicesPermission(SecurityAction.InheritanceDemand, Unrestricted=true)] public class ActiveDirectoryMembershipProvider : MembershipProvider

{

...

[DirectoryServicesPermission(SecurityAction.Assert, Unrestricted=true)]

[DirectoryServicesPermission(SecurityAction.Demand, Unrestricted=true)]

[DirectoryServicesPermission(SecurityAction.InheritanceDemand, Unrestricted=true)] public override string ResetPassword(string username, string passwordAnswer)

...

}

In the case of individual public methods, the provider actually asserts DirectoryServices Permission at the same time it demands it. This cuts down on the overhead of walking the stack each time code in System.DirectoryServices or System.DirectoryServices.Protocols makes a demand. Because the declarative demand will already have verified that all of its callers have the necessary privileges, there is no reason to rerun the stack walk when the provider makes calls into classes from these namespaces.

If you drop the trust level of an ASP.NET application down to High trust, any of the previous examples will immediately fail with an error like the following:

Request for the permission of type ‘System.DirectoryServices.DirectoryServicesPermission, System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ failed.

512

ActiveDirectoryMembershipProvider

Thankfully, this error is at least clear enough to give you an idea of the problem, as well as a possible workaround. There are actually two approaches to getting the provider working again in partial trust:

Add DirectoryServicesPermission to the appropriate ASP.NET trust policy file (or create a custom trust policy with the permission).

Wrap all calls to the provider in a GAC’d assembly that asserts

DirectoryServicesPermission.

The first approach is definitely the easiest to implement, but it is also less secure. Broadly granting DirectoryServicesPermission to a partially trusted application means that anyone can write code to start accessing your directory servers. In essence, it takes away the layer of protection on the web server and means that you are depending on whatever ACLs you set on your directory servers to protect against a malicious developer trolling through your data.

If you are running in the High trust bucket though, this is effectively a trust bucket meant to be very much like Full trust, but without unmanaged code permissions. So, it isn’t unreasonable for a High trust application to use the first approach. You can modify the High trust policy file with the following:

<SecurityClass

Name=”DirectoryServicesPermission” Description=”System.DirectoryServices.DirectoryServicesPermission, ... “ />

...

<IPermission

class=”DirectoryServicesPermission”

version=”1” Unrestricted=”true” />

By now, these types of changes should be pretty familiar. Register DirectoryServicesPermission with a <SecurityClass /> entry in the <SecurityClasses /> element. Then inside of the XML element defining the ASP.NET named permission set, add the <IPermission /> element. With these two changes, your partial trust ASP.NET application will start working again when using

ActiveDirectoryMembershipProvider.

Using a wrapper assembly involves a little more work, but it is actually pretty simple to accomplish. Create a new class library project in Visual Studio, making sure to reference the following assemblies:

System.Configuration — Needed because the project will be creating a new provider.

System.Web — Because the custom provider will be deriving from ActiveDirectory MembershipProvider

System.DirectoryServices — This assembly contains the DirectoryServices Permission.

You will need to generate a key file and enable strong naming for the project. Because the intent of the wrapper assembly is to assert DirectoryServicesPermission on behalf of partially trusted applications, you also need to add the APTCA attribute to AssemblyInfo.cs:

513

Chapter 12

using System.Security;

...

[assembly: AllowPartiallyTrustedCallers()]

With these basic tasks completed, you can now “write” the wrapper provider. In reality, the wrapper provider is nothing more than a class definition where DirectoryServicesPermission can be asserted along with overrides for each of the methods you want available to partial trust applications.

using System;

using System.Configuration.Provider; using System.Security.Permissions; using System.Web.Security;

using System.DirectoryServices;

namespace ADProviderWrapper

{

[DirectoryServicesPermission(SecurityAction.Assert, Unrestricted=true)] public class ADProviderWrapper : ActiveDirectoryMembershipProvider

{

//You must always override Initialize public override void Initialize(string name,

System.Collections.Specialized.NameValueCollection config)

{

base.Initialize(name, config);

}

public override bool ChangePassword(string username, string oldPassword, string newPassword)

{

return base.ChangePassword(username, oldPassword, newPassword);

}

public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)

{

return base.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer);

}

//Additional overrides for methods you want available in partial trust

}

}

Code-wise, there isn’t anything complex going on here. You start out referencing all of the related namespaces, derive from ActiveDirectoryMembershipProvider and then override the methods that you care about. The declarative assertion on the class means the common language runtime (CLR) will automatically assert this permission for any method that the class implements. The only method that you are required to override is the Initialize method. Because Initialize is always called when the Membership feature is instantiating providers based on configuration, you have to make sure the custom provider’s implementation is called first in order to get the permission assertion onto the stack.

514

ActiveDirectoryMembershipProvider

Other than the Initialize method, you can override whichever methods you care about exposing to partial trust applications. If your intent is to use all of the functionality of ActiveDirectory MembershipProvider from partial trust, then you would override all of the public methods on the provider. You might think that just adding the assertion for DirectoryServicesPermission would be sufficient and that you could avoid overriding any individual methods. Because the ActiveDirectory MembershipProvider has a class level link demand though, any method that is not overridden means that the Framework will evaluate the link demand against the code that is directly calling it. Of course, for partial trust applications, this means that your partially trusted page code will be the immediate caller, and hence without an intervening override from the custom provider sitting on the call stack, the link demand will fail.

After you compile the custom provider and install it in the GAC, you can modify your partial trust application to use it:

<trust level=”High” />

<compilation>

<assemblies>

<add assembly=”ADProviderWrapper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b95a0989e24f0920”/>

</assemblies>

</compilation>

<membership defaultProvider=”gacdprovider”> <providers>

<clear/>

<add name=”gacdprovider” type=”ADProviderWrapper.ADProviderWrapper,ADProviderWrapper,

Version=1.0.0.0, Culture=neutral, PublicKeyToken=b95a0989e24f0920” enableSearchMethods=”true” connectionStringName=”directoryconnection”/>

</providers>

</membership>

The <assemblies /> directive makes the ASP.NET application aware of the custom provider sitting in the GAC. The <membership /> section adds the GAC’d provider and indicates that it should be used as the default provider for the Membership feature. At this point, you can run your partial trust application and make use of the functionality in ActiveDirectoryMembershipProvider without running into any security exceptions. From the point of view of the application developer, using the GAC’d provider is no different than using the base provider. The nice thing about using the GAC’d provider is that you have the ability to customize the subset of functionality on ActiveDirectoryMembershipProvider that you want to make available in your partial trust applications. For example, you could create a custom provider that only asserts permissions for read-oriented methods like ValidateUser, while choosing not to override more sensitive methods like ChangePassword or ResetPassword.

Summar y

ActiveDirectoryMembershipProvider works with both AD and ADAM directory stores. The provider implements all of the functionality of the Membership API with the following two exceptions: the provider does not keep track of users that are online, and the provider does not support password

515

Chapter 12

retrieval. You should probably invest some time planning for deploying and using the provider, especially in complex domain environments. When running against AD ActiveDirectoryMembership Provider works in the scope of either a single domain, or a container within a domain. You can still leverage the provider in multidomain scenarios, but you will need to configure at least one provider instance per domain that you need to work with. Within the scope of a single domain, you can choose to point the provider at the root of the domain (that is, the default naming context), or at a specific container within the domain. In the case of ADAM, though, you always have an application partition, so for ADAM the provider will at least always be working in the context of the application partition (which itself is a container). As with AD, you can also configure containers in ADAM and have the provider work within the context of these containers.

After you have settled on which domain and/or container you are working with, the next major decision is the type of username you plan to support. For ADAM, the username in the Membership feature will always map to the userPrincipalName attribute in the directory. For Active Directory, you can choose to use either the userPrincipalName or the sAMAccountName attribute. Applications using older directories that were upgraded from NT4 will likely need to switch the provider to use sAMAccountName. The provider automatically maps other directory attributes to the various properties on a MembershipUser instance. A small subset of the MembershipUser properties can have these attribute mappings changed from their defaults. If you choose to enable password resets for the provider (not enabled by default), then you will need to edit the directory schema in order to store the question and answer as well as the bad password answer tracking information.

Although securing AD and ADAM is an entire topic in and of itself, there are two main security decisions to keep in mind when using ActiveDirectoryMembershipProvider. By default, the provider attempts to establish a secure connection with AD or ADAM. In the case of AD, this will normally “just work.” For ADAM, though, you need to explicitly configure SSL support on the ADAM server and on the web servers for the provider to be able to securely connect to the directory. The other aspect of security to consider is locking down read and write access to user objects in the directory. If at all possible, you should plan on storing different user populations in different OUs in your directory, and you should also delegate control over those OUs to specific accounts. You can then configure different provider instances using different sets of explicit credentials that only have selected rights in a specific OU.

Although ActiveDirectoryMembershipProvider ships as part of ASP.NET, it has been tested and is supported for use in non-ASP.NET environments as well. For both ASP.NET and non-ASP.NET

environments, though, the provider will only work in full trust by default. In partial trust ASP.NET environments, you do have the option of adding the DirectoryServices permission to a trust policy file. However, the more secure approach for any partial trust environment is to wrap access to the provider inside of a GAC’d assembly.

516

Role Manager

Role Manager is a new feature in ASP.NET 2.0 that provides the basic functionality necessary to create an IPrincipal-based object associated with roles. The motivation for the Role Manager feature is to make it easy for developers to associate users with roles and then perform role checks both declaratively and in code. The Role Manager feature is sometimes referred to as a companion feature to Membership because Role Manager can be used to provide authorization for users that have been authenticated using Membership. However, Role Manager can also be used as a standalone feature that integrates with other authentication mechanisms, including Windows authentication.

As with the Membership feature, Role Manager can be used in non-ASP.NET environments such as the Winforms application and console applications, thus making it easier for developers to share a common set of authenticated users and role information across different client applications. This chapter will cover:

The Role class

The RolePrincipal class

The RoleManager model

RoleProvider

WindowsTokenRoleProvider

The Roles Class

As with the Membership feature, the Role Manager feature has a static class that can be used as an easy way to access the functionality of the feature. The Roles class has methods and properties that cover the following areas:

Chapter 13

Public properties that primarily expose the Role Manager data from configuration

Public methods that act as facades on top of the default Role Manager provider

A single utility method that you can use for clearing the Role Manager cookie

Because most ASP.NET provider-based features follow the same general design, I won’t rehash how default providers work or the concept of façade methods mapping to the default provider. These areas work the same way in Role Manager as was described earlier in Chapter 10, which discussed Membership.

Regardless of where Role Manager is used, the feature always requires at least Low trust to work. This means that either an ASP.NET application must run in Low trust or higher to use the feature or, for a non-ASP.NET application, the AspNetHostingPermission must be granted to the calling code with a level or Low or higher.

The public properties on the Roles class for the most part just mirror the configuration settings from configuration. Some of the properties should be familiar to you because they work the exact same way on the static Membership class. Properties that are provider-specific or that involve unique behavior to the Role Manager features are described below.

Provider — Returns a RoleProvider reference to the provider defined by the defaultProvider attribute on the <roleManager /> configuration element.

Providers — Returns a RoleProviderCollection containing one reference to each provider defined within the <providers /> element contained within a <roleManager /> element.

ApplicationName — Returns the value of the applicationName provider configuration attribute for the default provider.

Enabled — Returns true if the Role Manager feature is enabled. The concept of being “enabled” though is based upon two different factors: the “enabled” attribute from the <roleManager /> configuration element, and the current trust level. Unlike Membership, you can go into configuration and explicitly disable the Role Manager feature (effectively, the Membership feature is always “on”). In fact, the default configuration of the Role Manager feature is disabled — you won’t see this in machine.config, but the hard-coded value for the “enabled” attribute in the

RoleManagerSection class is false. Because machine.config does not redefine this attribute, Role Manager is turned off by default on all machines. Assuming that you explicitly enable the Role Manager feature by setting the “enabled” attribute to true, you still need to be running in Low trust or higher. If you are running in Minimal trust, Roles.Enabled will always return false, regardless of the setting in configuration. This is done because Role Manager (and for that matter Membership) is not intended for use in Minimal trust applications.

CacheRolesInCookie — By default, this value is set to false. If it is set to true, the RoleManagerModule attempts to improve the performance of the Role Manager feature by caching the roles for a user within a cookie and using the cookie during subsequent page hits. Cookie caching is covered in detail in the section on the RoleManagerModule.

MaxCachedResults — The maximum number of roles that the RoleManagerModule will attempt to stuff into a cookie, assuming that CacheRolesInCookie is set to true. Because cookies are usually limited in size to around 4KB, you can use this setting to proactively hint the module so that it doesn’t waste time attempting to pack enormous numbers of roles into a cookie.

518

Role Manager

There are seven more public properties on the Roles class, but I won’t list them here because these additional properties all deal with the roles cookie. The corresponding configuration attributes are covered a little later in the section on role cache cookie settings. If you are familiar with the cookie options for Forms Authentication in ASP.NET 2.0, then the cookie settings available from the Roles class will make sense. For the most part, they control the same set of functionality (that is, cookie name, path, protection, and so on) as Forms Authentication. The one minor difference is that, unlike Forms Authentication, the Role Manager feature only supports the use of cookies for caching roles. There is no such thing as caching a user’s roles in a cookieless value on the URL. The effective 4KB upper limit is already constraining for some Role Manager scenarios — attempting to cram cached roles into a path segment with an upper limit of 255 characters just wouldn’t work.

Aside from the façade methods that provide easy-to-use method overloads for the default RoleProvider, there is one other method of interest on the Roles class: DeleteCookie. As the method name suggests, after you call this method the Roles class sends a clear cookie back to the browser that forces the Role Manager cookie in the browser to be deleted. Of course, if you never use cookie caching with Role Manager, you will never have a reason to call this method. However, if you create a logout page for your users, you should call Roles.DeleteCookie after clearing the authentication information as well:

//Logout page logic FormsAuthentication.SignOut(); Roles.DeleteCookie();

//Additional logic to prevent forms cookie re-use – see Chapter 5

If you forget to call Roles.DeleteCookie from your logout page it isn’t the end of the world. The RoleManagerModule responsible for handling the cookie is smart enough to ignore and clear any role cookies sent by anonymous users. So if you have a role cookie lying around in the browser after a logout, the next time a user hits your site the RoleManagerModule will automatically call

DeleteCookie.

One thing that developers sometimes look for when they start working with the Role Manager feature and the Roles class is some kind of role object. For ASP.NET 2.0, a role is just a string value — there is no rich object model for representing a role or manipulating a role. As a result, when you use the Roles class, you can see that most of the method parameters are just strings. You associate users (represented as a string username and an implicit application name) with role names. If ASP.NET ever creates a rich role object in a future release, it will probably require a substantial overhaul to the Role Manager feature because the current implementation is so tightly tied to the basic concept of a role as a string.

The façade methods include some extra logic for the case that the Roles class is called when the current user is represented by a RolePrincipal, and the method calls on the Roles class potentially affects that user. This allows the Roles class to take advantage of the caching behavior in the RolePrincipal class. For web applications, the current user is determined by looking at HttpContext.Current.User.

Because the Role Manager feature is also supported for non-ASP.NET applications, the Roles class will look for the current user object in Thread.CurrentPrincipal for non-web applications.

This means that if you want the full functionality of the Role Manager feature to work consistently outside of ASP.NET, you should write some code that initializes Thread.CurrentPrincipal with a RolePrincipal for the current user of your application. As is described in the next section on

RolePrincipal, you can create a RolePrincipal that wraps a WindowsIdentity. This means that you can have a fat-client application that requires a logged in Windows user but that fetches applicationspecific role information using the Role Manager feature.

519

Chapter 13

The interaction between Roles and RolePrincipal is described here for each of the relevant façade methods:

IsUserInRole — If the current user is a RolePrincipal, and the username parameter to this method matches the username (that is, IIdentity.Name) for the RolePrincipal, and the name of the provider associated with this RolePrincipal matches the name of the default Role Manager provider, the façade method instead calls RolePrincipal.IsInRole. The string comparison for username is a case-insensitive ordinal-based comparison. However, if the username or provider name of the RolePrincipal don’t match the username parameter to the method or the name of the default provider, then Roles.IsUserInRole calls the default RoleProvider instead. Note that for the parameterless IsUserInRole overload, the username is taken from HttpContext.Current.User. So if a RolePrincipal is attached to the context for this case, the parameterless IsUserInRole overload usually results in a call to

RolePrincipal.IsInRole instead.

GetRolesForUser — This method has the same behavior as IsUserInRole. If the current user is a RolePrincipal, and all of the other data matches, then the Roles class calls

RolePrincipal.GetRoles. Otherwise, the Roles class calls the GetRolesForUser method on the default provider.

DeleteRole — This method checks to see whether the current user is a RolePrincipal and if the RolePrincipal object uses the default provider. If both of these conditions are met, and if the RolePrincipal instance has cached role information within itself, the method checks to see whether the user represented by the principal belongs to the role that is being deleted. If this is the case, the method invalidates the RolePrincipal cache by calling RolePrincipal

.SetDirty. Normally, you see this behavior only if you are in a management application and you change the role membership for the user that you are currently logged in as.

AddUserToRole, AddUsersToRoles, AddUserToRoles, AddUsersToRole — All of these methods have logic similar to DeleteRole. If necessary, the current user represented by a RolePrincipal has its internal cache flushed if that user was added to a role using any of these methods. As with DeleteRole, you will probably only see this behavior when you are changing user-to-role assignments for yourself, and you are logged in to an administrative application as yourself.

RemoveUserFromRole, RemoveUsersFromRoles, RemoveUserFromRoles,

RemoveUsersFromRole — These methods follow the same logic as described in the last two bullet points. In this case, the RolePrincipal cache is flushed if the current user has been removed from a role using any of these methods.

Just like the Membership feature, the Role Manager feature also has the concept of a primary key. The username and the application name from a provider’s configuration are combined and used as the “primary key” when working with users and roles. See the section “The Primary Key for Membership” in Chapter 10 for a detailed discussion of how the username and application name are used to reference users. The only difference between Membership and Role Manager in this respect is that only the Membership feature went so far as to expose data-store-specific primary keys in its public APIs. The Role Manager feature doesn’t do this — instead both the Roles class and the RoleProvider base class reference users with just a string username and roles with just a string role name. (The application name is implicitly used as well because it is obtained from a provider’s configuration.)

520

Role Manager

The RolePrincipal Class

Because the Role Manager feature’s main purpose is to supply an IPrincipal based object, it includes an implementation of this interface with the RolePrincipal class. The RolePrincipal is intended for use anywhere a Framework application (ASP.NET or non-ASP.NET) expects to find an IPrincipal for IsInRole calls. RolePrincipal also exposes some additional methods for retrieving all of a user’s roles as well as for handling some of the work necessary when using cookie caching.

public sealed class RolePrincipal : IPrincipal, ISerializable

{

//Constructors

public RolePrincipal(IIdentity identity, string encryptedTicket) public RolePrincipal(IIdentity identity)

public RolePrincipal(string providerName, IIdentity identity) public RolePrincipal(string providerName, IIdentity identity,

string encryptedTicket)

//Cookie caching related methods public string ToEncryptedTicket()

//Role Manager and IPrincipal related functionality public string[] GetRoles()

public bool IsInRole(string role)

public void SetDirty()

//Public properties not related to cookie caching public int Version { get; }

public IIdentity Identity { get; } public string ProviderName { get } public bool IsRoleListCached { get; }

//Public properties related to cookie caching public DateTime ExpireDate { get; }

public DateTime IssueDate { get; } public bool Expired {get; } public String CookiePath { get; }

public bool CachedListChanged { get; }

}

The first thing that may leap out at you is that RolePrincipal is sealed! This has important implications for more complex scenarios such as handling multiple RoleProviders in an application with cookie caching turned on. It also means that if you want to extend the principal to include custom functionality, you can’t. Hopefully, in a future release the RolePrincipal class will be unsealed.

As you can see, although the RolePrincipal class implements the IPrincipal interface, it provides quite a bit more functionality beyond just a simple role check. Take a look at the portion of RolePrincipal that deals strictly with role information. There are two constructor overloads that can be used to create a RolePrincipal when you aren’t using cookie caching. One constructor overload takes a single IIdentity parameter, while the second overload accepts both an IIdentity and the name of a provider.

521