Rolling Your Own Website Administration Tool - Part 1
By Dan Clem
Introduction
Forms-based authentication combined with ASP.NET 2.0's Membership and
Roles systems makes creating and managing user accounts incredibly easy. I continue to be amazed at how
the login-related Web controls encapsulate the array
of tasks that I had always had to code by hand in classic ASP. For more on the Membership and Roles systems, be sure to
read the Examining ASP.NET 2.0's Membership, Roles, and
Profiles article series.
To help administer users, roles, and authorization settings, ASP.NET 2.0 includes the Web
Site Administration Tool (WSAT). WSAT is available from the Visual Studio 2005 Website menu via the ASP.NET Configuration
menu option. Launching the WSAT from Visual Studio, however, allows only local websites to be administered.
Such restrictions are limiting when hosting a website remotely with a web hosting company. (Granted, the WSAT's files
are available in the %WINDOWS%\Microsoft.NET\Framework\v2.0.50727\ASP.NETWebAdminFiles folder and can be
deployed from there.)
Rather than move the existing WSAT tool to my remote host, I decided to build my own WSAT-like tool from the ground up.
My version duplicates all features inside the Security section of the WSAT and adds a useful "Access Rules Summary" view
of the website security as applied to any given user or role. The complete code can be downloaded from the end of this
article and added to your site within a matter of minutes. This article provides an overview of my custom WSAT implementation
and explores the user list and add and edit user pages in detail. Part 2 explores the role management
and access rules sections in detail. Read on to learn more!
Using My Custom Website Administration Tool
My complete customized WSAT application can be downloaded at the end of this article. The download includes a fully-functioning
shell of a dummy corporate intranet. Its global navigation menu contains one link for each department - IT, marketing,
sales, and so on - with each department's web pages existing as a separate, physical folder in the website.
This demo uses the SqlMembership provider, storing user information in the ASPNETDB.MDF database
in the application's App_Data folder. This database is a Microsoft SQL Server 2005 Express Edition database.
Follow these steps to get up and running with this sample application:
Copy the contents to the hard drive of your development machine, which must have both Visual Studio 2005 and SQL
Server Express installed. The free Visual Web Developer
version will work just fine.
Open Visual Studio.
Click File --> Open Web Site from the main menu, browse to the folder where you extracted the contents downloaded
at the end of this article, and open up the website.
Click the green arrow to start debugging. Visual Studio should start its built-in Web server and the login page
should be displayed.
Log in with user name "Dan Clem", password "dan" (omitting the quotation marks). Dan Clem is the only administrator
in the system. You can log in as a non-administrator using any of the following user names:
"Edward Eel", "Franklin Forester", "Gordy Gordon", "Harold Houdini", or "Ike Iverson". The
password for each of these users is their first name, all lower case. Note that non-administrators are unable
to visit the custom WSAT page.
Click the Admin link from the global navigation menu to begin reviewing the application. The other links on the
global navigation menu are simple placeholders for future development work on our dummy corporate intranet. They
are used to demonstrate the Access Rule Management and Access Rule Summary pages.
To add the custom WSAT application to an existing Membership and Roles-based application, perform the following steps:
Copy the admin folder into your website's folder structure so that it exists directly underneath the
root folder of your ASP.NET application. (In my application, I've mapped the ASP.NET application to the root website
folder, but it should still work if your ASP.NET application is a subfolder inside the website.)
Copy the Alphalinks User Control (alphalinks.ascx) into a suitable spot inside your website, then modify
the @Register directive on the users.aspx page to match its new location. (For a more
detailed discussion on configuring and working with User Controls, see An
Extensive Examination of User Controls.)
Copy the images found in the i folder to a suitable spot in your images folder, then modify the image links on the
access_rules.aspx and access_rule_summary.aspx pages accordingly.
Make certain to register the following namespaces in the system.web section of your web application's root web.config
file. They are needed for the DataTable and DirectoryInfo classes used by some of the web pages in the custom
WSAT application:
You'll need to modify the pages to fit your master page and navigational menu techniques. (I use a hybrid
ASP.NET 2.0/classic ASP approach, where the global navigation menu appears in a single master page that is shared
throughout the site, while the secondary navigation menus appear as include files specific to each subfolder.)
Note that I've split my admin folder into two subfolders: access and activity.
The custom WSAT application is fully contained inside the access folder. I haven't yet developed the
activity folder, but I intend to add web pages that provide a log of what users are viewing what pages.
Secure the admin folder to the appropriate Roles or Users using the steps outlined in the "Securing Our
Custom WSAT" section of this article.
If you are itching to get started with the application, feel free to go ahead and download, install, and test it. The
remainder of this article walks through the screens that list, add, and edit user accounts. Part 2 looks at
role management and specifying access rules. This article and the upcoming one serve as a general "developer's manual" for the
application.
Managing User Accounts
The Security section of the official ASP.NET 2.0 WSAT provides a set of pages for managing a website's users, roles, and
access rules. In creating my custom WSAT application, I have rebuilt all of the core WSAT pages inside this section. Additionally,
I added a useful "Access Rules Summary" page and other features that, in my opinion, improve the overall page flow and
usability. I started out by building a set of user list pages that I felt would cover the needs of my typical deployments.
I broke this out into separate pages for working with:
Users by Name
Users by Role
Active Users
Online Users
Locked Out Users
The screen shot below shows this page in action. The GridView lists the website's users and their details. The links along the
top of the "Users by Name" section allows you to view users by name or role, or limit the view to only active, online, or
locked out users. Moreover, the "User Name filter" allows you to pare down the list to those users whose name starts with
a particular letter.
Exploring the Users by Name View
In the Users by Name view the GridView is populated with the website's users via the following two lines of code:
The Membership class
is part of the .NET Framework and includes methods for retrieving information about users in the system. The
GetAllUsers() method,
for example, returns all users in the site.
I wanted to add an alphabetic filter to match the user interface found in the official WSAT. I decided to build this as an
ASP.NET User Control so that I could reuse it later. Because I'm just learning how to build ASP.NET controls, this task took a
bit longer than I planned, but I learned a couple things along the way, so allow me to share. I built two versions of
this User Control, which I've called Alphalinks. Coming from classic ASP, I still have a strong bias toward the simplicity of
QueryString-based navigation, so that's how I built the first version of this control. The control simply outputted all
letters of the alphabet as regular links, adding a QueryString parameter in the form &letter=X. This worked
great until I tried to use it alongside an ASP.NET DropDownList control whose AutoPostBack property
was set to True. I soon realized that combining the ASP.NET postback and traditional QueryString-based navigation models
opens a can of worms.
With yet another lesson learned and yet another minor heartbreak behind me, I said goodbye to the QueryString-based
navigation model and built a new version as a proper postback-based ASP.NET User Control. I won't go into the details here,
but I quickly realized an advantage that comes with postback-based controls: we can access the control values as properties
rather than using the Request.QueryString paradigm of the classic ASP world. In particular, I added a
Letter property to the Alphalinks User Control that returned the selected letter. With this property
added, the control could be used programmatically, like so:
A Look at the Users by Role View
The Users by Role view uses a GridView to list the users that belong to a specified role.
In place of the Alphalinks User Control, this page has a DropDownList that lists the roles in the system.
This DropDownList is populated with the roles using the Roles class's
GetAllRoles() method like so:
The filtering logic for this page is a trifle more complicated because there are no methods in the Membership or Roles sytems
that return the details for all users in a particular role. While the Roles class does have a
GetUsersInRole(roleName)
method, it returns just the usernames of the users in the specified role and not the users' details like their email
address, active status, last logon date, and so forth. I overcame the lack of such a method by writing code that returns
all user details and then all users in a particular role. I then loop through the usernames in the specified role
and add the corresponding user details to a filtered collection.
// Get all of the users
MembershipUserCollection allUsers = Membership.GetAllUsers();
MembershipUserCollection filteredUsers = new MembershipUserCollection();
if (UserRoles.SelectedIndex > 0)
{
// If we are filtering by role, get the users in the specified role
string[] usersInRole = Roles.GetUsersInRole(UserRoles.SelectedValue);
// For each user in the role, add the user details to filteredUsers
foreach (MembershipUser user in allUsers)
{
foreach (string userInRole in usersInRole)
{
if (userInRole == user.UserName)
{
filteredUsers.Add(user);
break; // Breaks out of the inner foreach loop to avoid unneeded checking.
}
}
}
}
else
{
// We are not filtering by role...
filteredUsers = allUsers;
}
// Bind the users to the Users GridView
Users.DataSource = filteredUsers;
Users.DataBind();
The final three user-related pages (Active Users, Online Users, and Locked Out Users) were all straightforward. The
filtered lists were created by getting all user details via Membership.GetAllUsers(), iterating through the
returned MembershipUsers collection, and checking the appropriate Boolean property of each
MembershipUser
object in the collection to determine whether to add it to a filtered collection. This filtered collection was then
bound to the GridView on the web page. Note that "Active" corresponds to the
IsApproved property of the
MembershipUser class, "Online" to IsOnline,
and "Locked Out" to IsLockedOut.
Alternative Approaches to Filtering Users
The logic used to filter users by role and by active, online, and locked out status all work the same, in general. They get
all user details by calling Membership.GetAllUsers() and then iterate through the resultant collection.
This approach, however, does not scale as the user accounts grow into the thousands and beyond. If you expect more than a
few hundred user accounts, consider ditching the built-in Membership API and querying your user store using more efficient
techniques.
For example, if you're using the SqlMembershipProvider, you could add stored procedures to the
database to return the details for all users in a particular role by doing a JOIN between the aspnet_Users,
aspnet_Membership, aspnet_Roles, and aspnet_UsersInRoles. Likewise, you would want
to employ custom paging logic to efficiently return specified subsets of user details to the GridView (see
Custom Paging in ASP.NET 2.0 with SQL Server 2005
for more information). While this approach would tightly couple your application to the specific Membership provider,
it would permit your application to scale with a burgeoning user base.
Creating New User Accounts
The Membership system makes it pretty easy to add new users via the Membership class's
CreateUser
method. My custom WSAT application contains an Add User page, which contains a CheckBoxList for Role selection
followed by the expected form fields for the pertinent new user attributes.
When designing the Membership system, Microsoft decided to capture only essential user information, things like the user's
name, their password, a comment about the user, their last login date, and so on. If we need to capture additional user-specific
fields for our application - such as gender, address, date of birth, and so on - we can either use the Profile system
or create our own infrastructure for storing this information. For more on ASP.NET 2.0's Profile system, see
Profiles in ASP.NET 2.0;
check out Erich Peterson's article,
Customizing the CreateUserWizard Control for an
example of storing additional user data in a custom table.
The following screen shot shows the Add User page in action:
The code to add a new user is simple: one line of code adds the user to the system; a second line then adds the comments
(because none of the CreateUser method overloads includes a comments parameter). After the user has been added,
a foreach loop associates the new user with each of the selected roles.
// Add Roles.
foreach (ListItem rolebox in UserRoles.Items)
{
if (rolebox.Selected)
{
Roles.AddUserToRole(username.Text, rolebox.Text);
}
}
}
Editing Existing User Accounts
The Edit User page is based on the design of the Add User page and can be accessed from the various user list pages. I
built this page using a CheckBoxList for the Roles and a DetailsView for the primary user info. Because the
CheckBoxList is separate from the DetailsView control, I had to enable/disable the checkboxes manually to make them stay
in sync with the built-in edit/view mode of the DetailsView:
private void Page_PreRender()
{
// Load the User Roles into checkboxes.
UserRoles.DataSource = Roles.GetAllRoles();
UserRoles.DataBind();
// Disable checkboxes if appropriate:
if (UserInfo.CurrentMode != DetailsViewMode.Edit)
{
foreach (ListItem checkbox in UserRoles.Items)
{
checkbox.Enabled = false;
}
}
// Bind these checkboxes to the User's own set of roles.
string[] userRoles = Roles.GetRolesForUser(username);
foreach (string role in userRoles)
{
ListItem checkbox = UserRoles.Items.FindByValue(role);
checkbox.Selected = true;
}
}
I also had to write some code to add or remove the user from roles, as necessary. This code is called from the
OnItemUpdating event of the DetailsView.
private void UpdateUserRoles()
{
foreach (ListItem rolebox in UserRoles.Items)
{
if (rolebox.Selected)
{
if (!Roles.IsUserInRole(username, rolebox.Text))
{
Roles.AddUserToRole(username, rolebox.Text);
}
}
else
{
if (Roles.IsUserInRole(username, rolebox.Text))
{
Roles.RemoveUserFromRole(username, rolebox.Text);
}
}
}
}
Conclusion
This article provided an oview of my custom WSAT application (available for download at the end of this article), examining
the user list pages and add and edit user pages in detail. The pages explored in this article make up the user management
portion of the tool. There are also role management pages and web pages for specifying authorization rules. These topics
are covered in Part 2.