Wednesday, July 30, 2008

Advantage Role Provider

This post is one of a series of posts about the Advantage Providers for ASP.NET other posts in this series are listed below.

A Role Provider is another part of the ASP.NET Provider Model and is used in conjunction with a Membership Provider. Creating a custom Role Provider is much easier than creating a Membership Provider because it has far fewer properties and methods. This article will discuss some of the implementation details of an Advantage Role Provider. These providers are used with an ASP.NET web site which is configured to use forms authentication.

The Advantage Role Provider inherits the RoleProvider base class which will allow it to be used by the ASP.NET Login Controls. A Role Provider has only three public properties; ApplicationName, Description and Name. Each is set when configuring the provider in the Web.config file. It is important that the Membership and Role providers use the same application name since this is used to identify users and roles (see this post for more information on the data structure)

The public methods of the Role Provider include creating, deleting, finding and getting roles. The most commonly used function is IsUserInRole which is used to determine if a user has permissions to a particular folder on the site. Adding roles is done with CreateRole and users are assigned roles using AddUsersToRoles.

The CreateRole function adds a new record into the Roles table. It uses the ApplicationID which is set in the web.config file so several applications can use the same roles table. The method has two business rules; a check for commas (,) which are not allowed and a check to ensure the role name is unique for the given application.

   1: public override void CreateRole(string rolename)
   2: {
   3:    if (rolename.Contains(","))
   4:    {
   5:        throw new ArgumentException("Role names cannot contain commas.");
   6:    }
   7:  
   8:    if (RoleExists(rolename))
   9:    {
  10:        throw new ProviderException("Role name already exists.");
  11:    }
  12:  
  13:    AdsConnection conn = new AdsConnection(connectionString);
  14:    AdsCommand cmd = new AdsCommand("INSERT INTO Roles " +
  15:            " (Rolename, ApplicationID) " +
  16:            " Values(:Rolename, :ApplicationID)", conn);
  17:  
  18:    cmd.Parameters.Add("Rolename", System.Data.DbType.String).Value = rolename;
  19:    cmd.Parameters.Add("ApplicationID", System.Data.DbType.String).Value = pApplicationID;
  20:  
  21:    try
  22:    {
  23:        conn.Open();
  24:  
  25:        cmd.ExecuteNonQuery();
  26:    }
  27:    catch (AdsException e)
  28:    {
  29:        if (WriteExceptionsToEventLog)
  30:        {
  31:            WriteToEventLog(e, "CreateRole");
  32:        }
  33:        else
  34:        {
  35:            throw e;
  36:        }
  37:    }
  38:    finally
  39:    {
  40:        conn.Close();
  41:    }
  42: }

Once roles are created users can be added to the roles using the AddUsersToRoles method. This method uses string arrays which allow multiple users to be added to multiple roles using a single call. The method inserts records into the UsersInRoles table which provides the many-to-many relationship between the Users and Roles tables. Since the method can insert multiple records into the table a transaction is used to ensure an all or none situation. Just like the CreateUser method some business rules must be applied before inserting the records. A check to verify that the specified user(s) and role(s) exist is mad prior to inserting any records into the UsersInRoles table. Finally the role names and user names must be converted to their respective IDs which are unique since the same user name or role name may be used by another application.

   1: public override void AddUsersToRoles(string[] usernames, string[] rolenames)
   2: {
   3:     string[] roleIDs;
   4:     string tmpRoles = "";
   5:     string[] userIDs;
   6:     string tmpUsers = "";
   7:  
   8:     // Build a string array of the roleIDs
   9:     foreach (string rolename in rolenames)
  10:     {
  11:         if (!RoleExists(rolename))
  12:         {
  13:             throw new ProviderException("Role name not found.");
  14:         }
  15:         else
  16:         {
  17:             tmpRoles += GetRoleID(rolename) + ",";
  18:         }
  19:     }
  20:  
  21:     foreach (string username in usernames)
  22:     {
  23:         if (username.Contains(","))
  24:         {
  25:             throw new ArgumentException("User names cannot contain commas.");
  26:         }
  27:         else
  28:         {
  29:             tmpUsers += GetUserID(username) + ",";
  30:         }
  31:  
  32:         foreach (string rolename in rolenames)
  33:         {
  34:             if (IsUserInRole(username, rolename))
  35:             {
  36:                 throw new ProviderException("User is already in role.");
  37:             }
  38:         }
  39:     }
  40:  
  41:     // Remove trailing comma and setup the arrays of IDs  
  42:     tmpRoles = tmpRoles.Substring(0, tmpRoles.Length - 1);
  43:     roleIDs = tmpRoles.Split(',');
  44:     tmpUsers = tmpUsers.Substring(0, tmpUsers.Length - 1);
  45:     userIDs = tmpUsers.Split(',');
  46:  
  47:  
  48:     AdsConnection conn = new AdsConnection(connectionString);
  49:     AdsCommand cmd = new AdsCommand("INSERT INTO UsersInRoles " +
  50:             " (UserID, RoleID) Values(:UserID, :RoleID)", conn);
  51:  
  52:     AdsParameter userParm = cmd.Parameters.Add("UserID", System.Data.DbType.String);
  53:     AdsParameter roleParm = cmd.Parameters.Add("RoleID", System.Data.DbType.String);
  54:  
  55:     AdsTransaction tran = null;
  56:  
  57:     try
  58:     {
  59:         conn.Open();
  60:         tran = conn.BeginTransaction();
  61:         cmd.Transaction = tran;
  62:  
  63:         foreach (string username in userIDs)
  64:         {
  65:             foreach (string rolename in roleIDs)
  66:             {
  67:                 userParm.Value = username;
  68:                 roleParm.Value = rolename;
  69:                 cmd.ExecuteNonQuery();
  70:             }
  71:         }
  72:  
  73:         tran.Commit();
  74:     }
  75:     catch (AdsException e)
  76:     {
  77:         try
  78:         {
  79:             tran.Rollback();
  80:         }
  81:         catch { }
  82:  
  83:  
  84:         if (WriteExceptionsToEventLog)
  85:         {
  86:             WriteToEventLog(e, "AddUsersToRoles");
  87:         }
  88:         else
  89:         {
  90:             throw e;
  91:         }
  92:     }
  93:     finally
  94:     {
  95:         conn.Close();
  96:     }
  97: }

The IsUserInRole method takes a username and role name and searches the UsersInRoles table for a match. Just like with the AddUsersToRoles the username and role name must be converted to the unique IDs prior to performing the search. This is done with the GetUserID and GetRoleID helper functions.

   1: public override bool IsUserInRole(string username, string rolename)
   2: {
   3:     bool userIsInRole = false;
   4:  
   5:     AdsConnection conn = new AdsConnection(connectionString);
   6:     AdsCommand cmd = new AdsCommand("SELECT COUNT(*) FROM UsersInRoles " +
   7:             " WHERE UserID = :UserID AND RoleID = :RoleID", conn);
   8:  
   9:     cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = GetUserID(username);
  10:     cmd.Parameters.Add("RoleID", System.Data.DbType.String).Value = GetRoleID(rolename);
  11:  
  12:     try
  13:     {
  14:         conn.Open();
  15:  
  16:         int numRecs = (int)cmd.ExecuteScalar();
  17:  
  18:         if (numRecs > 0)
  19:         {
  20:             userIsInRole = true;
  21:         }
  22:     }
  23:     catch (AdsException e)
  24:     {
  25:         if (WriteExceptionsToEventLog)
  26:         {
  27:             WriteToEventLog(e, "IsUserInRole");
  28:         }
  29:         else
  30:         {
  31:             throw e;
  32:         }
  33:     }
  34:     finally
  35:     {
  36:         conn.Close();
  37:     }
  38:  
  39:     return userIsInRole;
  40: }

You can download the provider from CodeCentral on the DevZone under the WebApps category. You can also use this direct link to the Advantage ASP.NET Providers project. I'll be discussing the SiteMap Provider next.

No comments: