Published: Wednesday, December 19, 2001
Implementing Role-Based Security with ASP.NET, Part 2
By Darren Neimke
Read Part 1
In Part 1 we examined what role-based security is from a high-level.
Furthermore, we looked at the three important sections involved in such a security scheme: Identities, Roles,
and Principals. In this part we'll look at how to create our own roles.
Creating your own roles
' Global.asax event handler that fires upon attempting to authenticate the user
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
If Request.IsAuthenticated() Then
' create an array of roles for the current user
' these would most likely be dynamically read
' from the data store for each user.
Dim arrRoles() As String = {"Manager", "Cleaner"}
' Add our Principal to the current context
Thread.CurrentPrincipal = New GenericPrincipal(Context.User.Identity, arrRoles)
End If
End Sub
|
This only needs to be done once because the runtime automatically copies a reference to the principal
object from the calling thread to the CallContext of the new thread.
If you cast your mind back to how FormsAuthentication works, you will remember that a user is authenticated and then an authentication cookie attached to the validating Response when you call one of the appropriate static methods of the FormsAuthentication provider. It is at that moment that the
Application_AuthenticateRequest handler is called.
When authenticating users you do not have to do anything in this handler, nor do you need to explicitly create
a new instance of the GenericPrincipal class. However, to dynamically assign custom roles to
a user (which is what we are interested in today) you do need to, and the Application_AuthenticateRequest
handler is the ideal place to do this.
Bringing it All Together
I'll stop here briefly, before we move onto the really cool stuff, to summarize the five steps needed to
allow your application to implement role based checking:
- Validate the User - check the users credentials against a data store of credentials.
- Create an Identity
Dim objIdentity As GenericIdentity = New GenericIdentity("txtUsername")
|
- Get the roles for the current user
Dim strRoles() As String
Dim arrRoles As New ArrayList()
' do some database call that returns a reader
While reader.Read()
arrRoles.Add(reader("role"))
End While
strRoles = arrRoles.ToArray(GetType(String), String())
|
- Add the Identity and the Roles to a Principal
Dim objPrincipal As GenericPrincipal = New GenericPrincipal(objIdentity, strRoles)
|
- Add the Principal to the current context of the current thread
Thread.CurrentPrincipal = objPrincipal
|
Using Roles
Once you have authenticated a user and defined their identity and roles there are four ways that you can
interact with the Principal to enforce permissions based on memberships within the application:
- Configuratively
- Programmatically
- Imperatively
- Declaratively
Configuratively
You can configure the authorization and/or the location elements within the Web.config configuration files
to grant or deny access to entire areas within the application outright, as opposed to the other 3 methods which suit a more piecemeal approach.
For example, in the application that we are developing it may be necessary to ensure that only users acting as System Administrators or Moderators are allowed access to certain administrative screens.
To demonstrate this, let's presume that all of the forms that allow users to moderate forum posts reside under a directory called "ForumAdmin". Rather than programatically checking the role of a user inside every file that sits in that directory, you can simply configure the Web.config file for that folder to do the checking for you.
The following snippet excludes all users except for System Administrators or Moderators from accessing files in the directory that the Web.config file resides in (as well as its subdirectories):
' Snippet from Web.config
<authorization>
<deny users="*" />
<allow roles="SysAdmin, Moderator" />
</authorization>
|
By placing the abovementioned Web.config at the root of the ForumAdmin directory,
the application itself will inspect the roles of the user making the Request to determine whether access is
granted. That's right, no additional code is neccessary, regardless of how many .aspx files are
placed under that folder. (Compare that to classic ASP and the Session-based approach, where every "sensitive"
page needs to have a check at the top of the page to determine if the user has rights to view the page...)
Even from this simple example, you should get an idea of how this new approach simplifies the task of authorization, and, how it supersedes the methods available under the current practices of classic ASP.
Programmatically
Alternatively, you may have directories that contain files that will be accessed by users acting in a wide
variety of roles. An example of this would be the directory that displays the actual forum itself. You need
to allow viewing access to all areas of the forum, even to unauthenticated members of the public, but to
create a new post, it is imperative that a user not be in the role of Public or Other.
In your code, whenever you need to check to see if the user is in a role you simply query the IsInRole method of the current user.
Let's look at the code snippet responsible for displaying the link to create a new forum post:
If Not (User.IsInRole("Public")) And Not (User.IsInRole("Other")) Then
' Display the link
Else
' Don't display it!
End If
|
Or said in another manner:
If Not (Thread.CurrentPrincipal.IsInRole("Public")) _
And Not (Thread.CurrentPrincipal.IsInRole("Other")) Then
' Display the link
Else
' Don't display it!
End If
|
Imperatively
You can also imperatively demand that a user be in a role to access code by creating an instance of the
PrincipalPermission class, configuring it with the user and role that you are checking for,
then call the Demand method of that object to do the check. If the check fails, a
SecurityException is raised. This is the previous example re-written to use imperative checking:
Dim objPermission As New PrincipalPermission(User.Identity.Name, "manager")
Try
objPermission.Demand()
Catch ex As SecurityException
' Don't display it!
End Try
|
The downside to the two previous methods is that if you are calling a method several times from different parts of the application, you need to repeat this logic all over the place.
Declaratively
As many of you would know, one of the great benefits of using Stored Procedures in the database is that you
can enforce permissions specific to an individual stored procedure. With declarative checks you can bind permissions to an actual method (or event, or whatever) using meta-data attached to each specific object that requires it.
For our purposes, let us assume that we have a method called dismissModerator(). Now, for
obvious reasons, we do not want the moderator, or anyone else for that matter, to be able to call this method!
Therefore, only users who are in the role of System Administrator can access this method. Let's set it up:
' Create a method that disables all moderator permissions
' and attach a PrincipalPermissionAttribute to it that issues
' a Demand.
<PrincipalPermissionAttribute(SecurityAction.Demand, Name:="smith", Role:="SysAdmin")> _
Public Sub DismissModerator()
' logic here
End Sub
|
And now, to call it:
Try
DismissModerator()
Catch ex As SecurityException
' do something else
End Try
|
What has happened here is that the Permission check that is bound to the DismissModerator() Sub
is carried out before execution of the Sub takes place. As you can see, this method of authorization is good,
because you are enforcing the policy on an actual object, meaning that a developer cannot accidentaly call
the method and inadvertently dismiss the poor forum moderator, because the object itself does the role checking.
You can enforce declarative checking at class, class member, property or even event level. If you define a
permission attribute on a class as well as one of its members the declaration at member level overrides the
declaration at class level.
Summary
In this article we've had a brief look at what role based security is and how the .NET runtime can assist in
implementing it in our solutions.
If you found this article interesting I'd encourage you to delve further into the
System.Security.Permissions namespace where you'll find even more classes to assist you in
producing a customized solution. Pay particular attention to the Union method of the
PrincipalPermission class which allows you to bind multiple principal permissions to create
a new permission.
Happy Programming!
By Darren Neimke
Download the role-based security sample application