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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 14

Figure 14-2

After you click the OK button, the Framework will consider all applications running in the LocalIntranet zone to be associated with the set of custom permissions listed in the XML file. Because applications running off of UNC shares are considered part of the local intranet zone, when you run the sample application for a second time from a remote UNC share all of the calls into Role Manager and the SqlRoleProvider succeed. Note that if you try this on your machine and the console application still fails, the definition for the Local Intranet zone in Internet Explorer may not include your remote machine. If you modify the Local Intranet zone definition in Internet Explorer to include a file://your_remote_machine URL, then the Framework will consider applications running remotely from that machine to be in the Local Intranet zone.

So, although this is a somewhat painful process, the end result is that you can absolutely use Role Manager inside of a partially trusted non-ASP.NET application. This means that you don’t have to drop back to granting unmanaged code rights to your non-ASP.NET applications just because of the use of

AspNetHostingPermission and other permissions like SqlClientPermission. After you create a custom named permission set and associate it with the local intranet zone, you will also be able to use the two other ASP.NET features that have been tweaked to work in non-ASP.NET environments: the Membership and the Profile features. Last, note that although this sample cloned the local intranet zone’s permissions, you can be more creative with your customizations. For example, you could strip some of the extraneous permissions from the custom permission set (for example, maybe you don’t need printer access or the ability to display file selection dialog boxes). You could also create custom code groups with more granular membership conditions than what is defined for the local intranet zone.

562

SqlRoleProvider

Database Security

Chapter 11 discussed the general database security requirements that are common to all of the SQLbased providers. Assuming that you have followed those steps, and you have a login created or mapped on your SQL Server machine, there are three database roles that you can use with SqlRoleProvider:

aspnet_Roles_BasicAccess — This role only allows you to call the following methods on

SqlRoleProvider: IsUserInRole and GetRolesForUser. These two methods represent the bare minimum needed to support the RolePrincipal object and authorization checks made directly against the provider.

aspnet_Roles_ReportingAccess — This role allows you to call IsUserInRole, GetRolesForUser,

RoleExists, GetUsersInRole, FindUsersInRole, and GetAllRoles. Members of this role can also issue select statements against the database views.

aspnet_Roles_FullAccess — This role can call any of the methods defined on SqlRoleProvider as well as query any of the database views. In other words, a SQL Server login added to this role has the additionally ability to change the role data stored in the database.

As with the SqlMembershipProvider, the simplest way to use these roles is to add the appropriate SQL Server login account to the aspnet_Roles_FullAccess role. This gives you the full functionality of the feature without requiring you to run with DBO privileges in the database. The other two SQL Server roles allow for more granular allocation of security permissions. For example, you might run administrative tools in one web application (which would use aspnet_Roles_FullAccess), while only performing authorization checks in your production application (which thus would only need aspnet_Roles_BasicAccess).

Working with Windows Authentication

Although the most likely scenario that folks think of for SqlRoleProvider is to use it in applications with forms authentication, SqlRoleProvider and the Role Manager feature work perfectly fine in applications using Windows authentication. Typically, you would use NT groups or more advanced authorization stores such as Authorization Manager for many intranet production applications. However, it is not uncommon for developers to create intranet applications in which they do not want or need the overhead of setting up and maintaining group information in a directory store. This can be the case for specialized applications that have only a small number of users, and it can also be the case for “throw-away” intranet applications.

Although I wouldn’t advocate using SqlRoleProvider for long-lived internal applications or for complex line of business applications, knowing that you can use Role Manager for intranet applications adds another option to your toolbox for quickly building internal websites with reasonable authorization requirements. In the case of a web application using Windows authentication, SqlRoleProvider will automatically create a row in the common aspnet_Users table the very first time a Windows user is associated with a role. The important thing is to use the correct format for the username when adding users to roles or removing users from roles. The username that is available from HttpContext.Current

.User.Identity.Name is the string that should be used when modifying a user’s role associations.

For example, the following code snippet shows how to add a domain user to two roles stored in a SQL database with the SqlRoleProvider:

563

Chapter 14

if (!Roles.IsUserInRole(“CORSAIR\\demouser”, “Application Role A”)) Roles.AddUserToRole(“CORSAIR\\demouser”, “Application Role A”);

if (!Roles.IsUserInRole(“CORSAIR\\demouser”, “Application Role C”)) Roles.AddUserToRole(“CORSAIR\\demouser”, “Application Role C”);

Note how the username is supplied using the familiar DOMAIN\USERNAME format. When you use Windows authentication in ASP.NET, the WindowsIdentity that is placed on the context will return the Name property using this format. If your web application is configured to use Windows authentication, when you enable the Role Manager feature, RoleManagerModule will automatically use the default provider to fetch the roles associated with the Windows authenticated user. The following configuration snippets show the required configuration to make this work:

<!-- connection string config and other config here -->

<authentication mode=”Windows”/> <authorization>

<deny users=”?”/> </authorization>

<roleManager enabled=”true”> <providers>

<clear/>

<add name=”AspNetSqlRoleProvider” type=”System.Web.Security.SqlRoleProvider, System.Web... “ connectionStringName=”LocalSqlServer” applicationName=”WindowsAuthenticationDemo”/>

</providers>

</roleManager>

Now, if you access a Windows authenticated web application as a user who has already been mapped to one or more roles, the RolePrincipal placed on the context will contain the expected role information.

foreach (string s in ((RolePrincipal)User).GetRoles())

Response.Write(User.Identity.Name + “ belongs to <b>” + s + “</b><br />”);

Running this code sample while logged in as the sample user that was configured earlier results in the following output:

CORSAIR\demouser belongs to Application Role A

CORSAIR\demouser belongs to Application Role C

The only minor shortcoming that you will encounter getting this to work is that you will have to programmatically associate Windows users to roles. Although the Web Administration Tool inside of Visual Studio allows you to create and delete roles, you won’t be able to leverage the tool for managing specific Windows users. Instead, you will need to use code like the sample shown earlier to add users to roles as well as removing users from roles.

One other concern you may have is keeping the format of the username stable over time. For the 2.0 version of the Framework, the WindowsIdentity class will always return the value from the Name property using the DOMAIN\USERNAME format. Even if someone accesses your application with a different username format (for example, your application is configured to use Basic authentication in IIS and some-

564

SqlRoleProvider

one logs in using a UPN formatted username), WindowsIdentity always uses the older NT4-style username. As a result, you don’t need to worry about accruing large amounts of user-to-role associations in a database only to find out that the username returned from WindowsIdentity suddenly changes on you.

For example, if you are running in a domain environment on Windows Server 2003 (that is, your domain controllers are Windows Server 2003 machines), you can run the following code sample:

WindowsIdentity wi = new WindowsIdentity(“demouser@corsair.com”);

Response.Write(wi.Name);

Even though the WindowsIdentity is constructed with a user principal name (UPN) format, the value returned by the Name property is still CORSAIR\demouser.

Running with a Limited Set of Roles

Typically, most of the users on a website are associated with a set of roles that make sense for their given purpose on the site. A limited number of website users, though, may have super privileges or the ability to act as an administrator on the site. Sometimes, it is desirable for this type of user to be able to limit the roles that he or she a part of while performing the normal daily routine on a site. For example, a business user may also have administrative privileges on a site. During the normal workday though he or she really doesn’t need to have these privileges available and would rather perform most of the work as a normal user.

Because RolePrincipal depends on a provider for its role information, you can swap in a custom provider that supports the concept of a limited subset of roles being active at any given time for a specific user. As an example, you can create a derived version of SqlRoleProvider that is aware of role restrictions stored in the database. For convenience, I chose to store the set of role restrictions in the Comments property associated with a MembershipUser. You could certainly choose to store this type of role restriction in a different location, but because Membership is already available and has a convenient storage location for this type of information, the sample provider makes use of it. Because a RolePrincipal works exclusively with information returned from GetRolesForUser, the custom provider must override this method. Because a custom role provider should ideally also support at least IsUserInRole, the custom provider also provides the limited role functionality in an override of this method as well.

public class CustomRoleProvider : SqlRoleProvider

{

public CustomRoleProvider() {}

//overrides of GetRolesForUser and IsUserInRole

}

The custom provider works by looking at the set of restricted roles stored in MembershipUser.Comment. The string stored in this property is formatted as follows:

first restricted role;second restricted role; etc..

The custom provider converts this string into a string array by splitting the value on the semicolon character. For protection though, the custom provider always double-checks with SqlRoleProvider to ensure that the information stored in the Comments property is still considered a valid set of role associations by the

565

Chapter 14

provider. This prevents the problem where a set of restricted roles is stored in the MembershipUser, but then at a later point in time the user no longer belongs to some of those roles.

public override string[] GetRolesForUser(string username)

{

MembershipUser mu = Membership.GetUser(username);

//Anonymous user case if (mu == null)

return new string[0];

if (mu.Comment != null)

{

//Make sure user still belongs to the selected roles

string[] currentRoleMembership = base.GetRolesForUser(username); string[] restrictedRoles = mu.Comment.Split(“;”.ToCharArray());

List<string> confirmedRoles = new List<string>(); foreach (string role in restrictedRoles)

{

if (Array.IndexOf(currentRoleMembership, role) != -1) confirmedRoles.Add(role);

}

return confirmedRoles.ToArray();

}

else

{

return base.GetRolesForUser(username);

}

}

Just as with the SqlRoleProvider, the custom provider first checks to see if the user is anonymous. Assuming that you have never stored a MembershipUser object in the database for the username, the call to GetUser always returns null for anonymous users. If the user is authenticated, and if there is a set of restricted roles stored in the Comment property, then the custom provider parses the information from the property. Most of the work is just double-checking with the base provider that the set of roles the user currently belongs to still grants access to the roles listed in the Comment field. The end result of this processing is the subset of restricted roles that still apply to the user. Of course, if no restricted role information is stored in the Comment property, the custom provider defers to the base provider.

public override bool IsUserInRole(string username, string roleName)

{

MembershipUser mu = Membership.GetUser(username);

//Anonymous user case if (mu == null)

return false;

if (mu.Comment != null)

{

string[] restrictedRoles = mu.Comment.Split(“;”.ToCharArray());

if ((Array.IndexOf(restrictedRoles, roleName) != -1)

566

SqlRoleProvider

&& (base.IsUserInRole(username, roleName)) ) return true;

else

return false;

}

else

{

//No restriction is in effect

return base.IsUserInRole(username, roleName);

}

}

The IsUserInRole override follows the same general pattern as GetUserInRole. The only difference is that in this case only a single role (the roleName parameter) is checked. As with GetUserInRole the roleName parameter must be found both in the restricted set of roles for the user, as well as in the set of roles currently associated with the user in the database.

Now that you have a customized version of the SqlRoleProvider, you can try it out in a sample application. The configuration for the sample application requires authorization for all pages. It also enables Role Manager and enables cookie caching as well. When you first try to access the test page in the sample application, you will be redirected to a login page. After you are logged in, and thus you have

a RolePrincipal attached to the context, the test page allows a user to restrict itself to a subset of the current role membership.

...

<asp:ListBox ID=”lbxUserInRoles” runat=”server” SelectionMode=”Multiple” />

...

<asp:Button ID=”btnRestrictRole” Runat=”server” Text=”Restrict Role”

OnClick=”btnRestrictRole_Click” />

...

<asp:Button ID=”btnUndoRestriction” Runat=”server” Text=”Undo Role Restriction”

OnClick=”btnUndoRestriction_Click” />

...

<asp:Label ID=”lblStatus” Runat=”server” Text=”” />

...

<asp:Literal ID=”litIsInRoleTests” runat=”server” />

...

A list box is displayed that contains the current set of roles associated with the user. Two buttons are available: one to restrict the user to the subset of roles that you can choose from the list box, and a second button that allows you to undo the role restrictions. Toward the bottom of the page, there is a literal control that contains the results of multiple calls to RolePrincipal.IsInRole.

Displaying the set of roles for the current user is accomplished by calling the Roles class. Remember that the parameterless version of Roles.GetRolesForUser actually results in a call to the GetRoles method on the RolePrincipal attached to the context. This means the list of information reflects the set of role information that RolePrincipal has fetched from the custom provider.

lbxUserInRoles.DataSource = Roles.GetRolesForUser();

lbxUserInRoles.DataBind();

567

Chapter 14

To demonstrate the effect of the overridden IsUserInRole method, the page also dumps the result of making various authorization checks directly against the provider.

StringBuilder sb = new StringBuilder();

if (Roles.Provider.IsUserInRole(User.Identity.Name,”Role A”)) sb.Append(“User is in Role A <br/>”);

if (Roles.Provider.IsUserInRole(User.Identity.Name,”Role B”))

sb.Append(“User is in Role B <br/>”);

if (Roles.Provider.IsUserInRole(User.Identity.Name,”Role C”)) sb.Append(“User is in Role C <br/>”);

litIsInRoleTests.Text = sb.ToString();

Restricting a user to a subset of his or her available roles occurs when you click on the role restriction button.

protected void btnRestrictRole_Click(object sender, EventArgs e)

{

string restriction = String.Empty;

foreach (ListItem li in lbxUserInRoles.Items)

{

if (li.Selected == true) restriction += li.Value + “;”;

}

if (!String.IsNullOrEmpty(restriction))

restriction = restriction.Substring(0, restriction.Length - 1);

else

restriction = null;

MembershipUser mu = Membership.GetUser(); mu.Comment = restriction; Membership.UpdateUser(mu);

((RolePrincipal)User).SetDirty();

Response.Redirect(“~/default.aspx”);

}

Because the list box allows for multiple selections, you can choose one or more roles from the set of roles currently associated with the user. The code bundles up the selected items into a semicolon delimited string and then stores this information in MembershipUser.Comment. Note that the page code then calls SetDirty on the current RolePrincipal. Because the restricted roles have been set, it is necessary to tell the RolePrincipal that it should ignore any currently cached information, and that instead it should refresh this information from the provider. The final redirect forces the page to be re-requested by the browser so that you can see the effect of restricting the roles.

You can undo the role restriction by clicking on the second button:

protected void btnUndoRestriction_Click(object sender, EventArgs e)

{

MembershipUser mu = Membership.GetUser();

568

SqlRoleProvider

mu.Comment = null; Membership.UpdateUser(mu);

((RolePrincipal)User).SetDirty();

Response.Redirect(“~/default.aspx”);

}

The page code simply nulls the information in MembershipUser.Comment. Because the role information for the user has changed, this code also tells the RolePrincipal to invalidate its cached information. After the redirect occurs, you will see that the user has reverted to the original set of role assignments.

If you use the Web Administration Tool (WAT) from Visual Studio, you can configure a test user and set up some role associations. For example, I created an account called “testuser” that belonged to three different roles. After you log in, the information displayed on the page looks like:

Listbox contains:

Role A

Role B

Role C

IsUserInRole checks:

User is in Role A

User is in Role B

User is in Role C

So far so good: the user belongs to all of the roles that you would expect, and currently the custom provider is just delegating the method calls to the base SqlRoleProvider. If you choose a subset of roles (choose only Role A and Role C), when the page refreshes, it reflects the restricted set of roles that the user belongs to.

Listbox contains:

Role A

Role C

IsUserInRole checks:

User is in Role A

User is in Role C

Now the user can only accomplish tasks on the site that are allowed to Role A and Role C. Even though in the database the user is also associated with Role B, from the point of view of the website the user no longer belongs to that role. You can see how with just the added logic in the derived version of SqlRoleProvider, the rest of the authorization code in a site is oblivious to the fact that a set of restricted roles is being enforced. If you click the button to undo the role restrictions, you will see that you return back to belonging to all of the original roles.

Although the sample just demonstrates the effect of role restrictions when calling RolePrincipal

.GetRoles and Roles.GetRolesForUser, with the changes made in the custom provider any type of website authorization mechanism that depends on HttpContext.Current.User will be affected. For example, any URL authorization checks will be transparently made against the restricted set of roles because URL authorization calls IsInRole on the principal object attached to the context. Similarly, if you had a site that made calls to IPrincipal.IsInRole, these authorization checks would automatically work with the restricted role functionality of the custom provider.

569

Chapter 14

Authorizing with Roles in the Data Layer

Because all of the user-to-role associations are stored in the database, and the SqlRoleProvider database schema includes SQL views that map to these tables, you can perform authorization checks in the database using this information. Depending on how your application is structured, you may find it to be more efficient to make a series of authorization checks in the database, as opposed to pulling information back up to the middle tier and then making a series of authorization checks using Role Manager. Older applications that have large amounts of their business logic still in stored procedures may need to keep their authorization logic in the database as well because it may be technically impossible to factor out the authorization checks to a middle tier.

As with the Membership feature, the first step you need to accomplish is the conversion of a (username, application name) pair to the GUID user identifier used in the database tables. You will want to store the result of converting an application name to a GUID identifier because you also need to convert a role name to its GUID identifier. Because role names are segmented by applications, just as usernames are partitioned by application, you will always be performing authorization checks in the context of a specific application name.

SQL Server 2000 conveniently supports user defined functions, so you can encapsulate all of this logic inside of a custom user-defined function.

create function IsUserInRole ( @pApplicationName nvarchar(256), @pUsername nvarchar(256), @pRolename nvarchar(256) ) returns bit

as begin

declare @retval bit

 

 

 

if exists (

 

 

 

select

1

 

 

from

dbo.vw_aspnet_Users u,

 

 

dbo.vw_aspnet_Applications a,

 

dbo.vw_aspnet_Roles r,

 

 

dbo.vw_aspnet_UsersInRoles uir

where

a.LoweredApplicationName = LOWER(@pApplicationName)

 

and

u.LoweredUserName = LOWER(@pUsername)

 

and

u.ApplicationId

= a.ApplicationId

 

and

r.ApplicationId

= a.ApplicationId

 

and

r.LoweredRoleName

= LOWER(@pRolename)

 

and

r.RoleId

= uir.RoleId

 

and

u.UserId

= uir.UserId

)

set @retval = 1

else

set @retval = 0

return @retval end

go

570

SqlRoleProvider

Much of the code in this function is the same as shown earlier in Chapter 11 in the getUserId stored procedures. The additional logic joins the @pApplicationName and @pRolename variables into the vw_aspnet_Roles view to convert from a string role name into the GUID identifier for the role. With the resulting role identifier, the select query looks in vw_aspnet_UsersInRoles for a row matching the

GUID identifiers that correspond to the user and role name in the requested application. If a row is found, the function returns a bit value of 1 (that is, true); otherwise, it returns a bit value of 0 (that is, false).

With this function, it is trivial to perform authorization checks in the data layer. The following code snippet makes an authorization check based on the user and role data that was created for the earlier sample on restricting a user’s roles:

declare @result bit

select @result = dbo.IsUserInRole(‘LimitingRoles’,’testuser’,’Role B’)

if @result = 1

print ‘User is in Role A’

Although performing authorization checks in the database is probably a rare occurrence given the types of application architectures in use today, it is still a handy tool to have available if you ever find that you need to authorize users from inside of your stored procedures.

Suppor ting Dynamic Applications

The RoleProvider base class defines the abstract property ApplicationName. As a result, you can use the same approach for supporting multiple applications on the fly with SqlRoleProvider as was shown earlier for SqlMembershipProvider. After you have a way to set the application name dynamically on a per-request basis, you can write a custom version of SqlRoleProvider that reads the application name from a special location. Remember that in Chapter 11 an HttpModule was used that looked on the query-string for a variable called appname. Depending on the existence of that variable as well as its value, the module would store the appropriate application name in HttpContext.Items [“ApplicationName”]. You can use the same module with a custom version of the SqlRoleProvider.

using System; using System.Web;

using System.Web.Security;

public class CustomRoleProvider : SqlRoleProvider

{

public override string ApplicationName

{

get

{

string appNameFromContext = (string)HttpContext.Current.Items[“ApplicationName”];

if (appNameFromContext != “NOTSET”) return appNameFromContext;

else

return base.ApplicationName;

}

}

}

571