Rolling Your Own Website Administration Tool - Part 1

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:

  1. 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.
  2. Open Visual Studio.
  3. 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.
  4. Click the green arrow to start debugging. Visual Studio should start its built-in Web server and the login page should be displayed.
  5. 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.
  6. 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:
  1. 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.)
  2. 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.)
  3. 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.
  4. 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:

    <pages>
       <namespaces>
          <add namespace="System.Data" />
          <add namespace="System.IO"/>
       </namespaces>
    </pages>

  5. 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.)
  6. 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.
  7. 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.

The users by name page.
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:

Users.DataSource = Membership.GetAllUsers();
Users.DataBind();

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:

Users.DataSource = Membership.FindUsersByName(Alphalinks.Letter + "%");

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:

UserRoles.DataSource = Roles.GetAllRoles();
UserRoles.DataBind();

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.

protected void AddUser()
{
   // Add User.
   MembershipUser newUser = Membership.CreateUser(username.Text, password.Text, email.Text);
   newUser.Comment = comment.Text;
   Membership.UpdateUser(newUser);
   
   // 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.

Until then... Happy Programming!

By Dan Clem
posted on 2007-06-11 08:46  心悦  阅读(779)  评论(0编辑  收藏  举报