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.

Monday, July 28, 2008

Advantage Membership 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.

ASP.NET includes a support for the Provider Model which contains a definition for a Membership Provider. This provider allows storage of user information into various forms. In this case the Advantage Membership Provider stores the user/member information in an Advantage Database.

The Membership Provider is invoked through the Membership class which will then communicate with Advantage (for details about the data structure see this post). Implementation of the provider is pretty straight forward and I already showed some of the functionality in my posts on Securely Storing Passwords and Using Salt. In this post I will discuss several of the public functions of the provider.

A Membership Provider has several properties which can be used to change the behavior of the provider. These properties are set in the Web.Config file when defining which provider to use. Most of these properties are used to determine password rules. The PasswordFormat property determines how the password is stored in the database valid options include; Clear, Encrypted and Hashed. There are also properties for allowing password reset (EnablePasswordReset), password length (MinRequiredPasswordLength) and the number of non-alphanumeric characters(MinRequiredNonAlphanumericCharacters). Additional information about these properties can be found here. Fortunately most of the properties do not require any special implementation to work with Advantage.

Nearly all of the public methods of the provider had to be overridden to be used with Advantage since these methods read and write to the data store. Direct user manipulation includes creating, updating, finding and deleting. Other functions include resetting passwords, unlocking and validating users. Descriptions for all of the public methods can be found here

The CreateUser function takes the provided user information and inserts a new record into the Membership table using the provided information. It outputs a MembershipCreateStatus which reports any problems with creating the user. The Advantage provider uses a GUID as the unique identifier for each user and also has a unique index on the username. The CreateUser function does a series of checks to ensure that the new user is unique before inserting the new record. A code sample is below:

   1: public override MembershipUser CreateUser(string username,
   2:              string password,
   3:              string email,
   4:              string passwordQuestion,
   5:              string passwordAnswer,
   6:              bool isApproved,
   7:              object providerUserKey,
   8:              out MembershipCreateStatus status)
   9:     {
  10:         ValidatePasswordEventArgs args =
  11:           new ValidatePasswordEventArgs(username, password, true);
  12:  
  13:         OnValidatingPassword(args);
  14:  
  15:         if (args.Cancel)
  16:         {
  17:             status = MembershipCreateStatus.InvalidPassword;
  18:             return null;
  19:         }
  20:  
  21:         if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
  22:         {
  23:             status = MembershipCreateStatus.DuplicateEmail;
  24:             return null;
  25:         }
  26:  
  27:         MembershipUser u = GetUser(username, false);
  28:  
  29:         if (u == null)
  30:         {
  31:             DateTime createDate = DateTime.Now;
  32:  
  33:             if (providerUserKey == null)
  34:             {
  35:                 providerUserKey = Guid.NewGuid();
  36:             }
  37:             else
  38:             {
  39:                 if (!(providerUserKey is Guid))
  40:                 {
  41:                     status = MembershipCreateStatus.InvalidProviderUserKey;
  42:                     return null;
  43:                 }
  44:             }
  45:  
  46:             AdsConnection conn = new AdsConnection(connectionString);
  47:             AdsCommand cmd = new AdsCommand("INSERT INTO Membership " +
  48:                   " (UserID, Username, Password, Email, PasswordQuestion, " +
  49:                   " PasswordAnswer, IsApproved," +
  50:                   " Comment, CreationDate, LastPasswordChangedDate, LastActivityDate," +
  51:                   " ApplicationID, IsLockedOut, LastLockedOutDate," +
  52:                   " FailedPasswordAttemptCount, FailedPasswordAttemptWindowStart, " +
  53:                   " FailedPasswordAnswerAttemptCount, FailedPasswordAnswerAttemptWindowStart)" +
  54:                   " Values(:UserID, :Username, :Password, :Email, :PasswordQuestion, :PasswordAnswer, " +
  55:                   ":IsApproved, :Comment, :CreationDate, :LastPasswordChangedDate, :LastActivityDate, " +
  56:                   ":ApplicationID, :IsLockedOut, :LastLockedOutDate, :FailedPasswordAttemptCount, " +
  57:                   ":FailedPasswordAttemptWindowStart, :FailedPasswordAnswerAttemptCount, :FailedPasswordAnswerAttemptWindowStart)", conn);
  58:  
  59:             cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey.ToString();
  60:             cmd.Parameters.Add("Username", System.Data.DbType.String).Value = username;
  61:             cmd.Parameters.Add("Password", System.Data.DbType.String).Value = EncodePassword(password, providerUserKey.ToString());
  62:             cmd.Parameters.Add("Email", System.Data.DbType.String).Value = email;
  63:             cmd.Parameters.Add("PasswordQuestion", System.Data.DbType.String).Value = passwordQuestion;
  64:             cmd.Parameters.Add("PasswordAnswer", System.Data.DbType.String).Value = EncodePassword(passwordAnswer, providerUserKey.ToString());
  65:             cmd.Parameters.Add("IsApproved", System.Data.DbType.Boolean).Value = isApproved;
  66:             cmd.Parameters.Add("Comment", System.Data.DbType.String).Value = "";
  67:             cmd.Parameters.Add("CreationDate", System.Data.DbType.DateTime).Value = createDate;
  68:             cmd.Parameters.Add("LastPasswordChangedDate", System.Data.DbType.DateTime).Value = createDate;
  69:             cmd.Parameters.Add("LastActivityDate", System.Data.DbType.DateTime).Value = createDate;
  70:             cmd.Parameters.Add("ApplicationID", System.Data.DbType.String).Value = pApplicationID;
  71:             cmd.Parameters.Add("IsLockedOut", System.Data.DbType.Boolean).Value = false;
  72:             cmd.Parameters.Add("LastLockedOutDate", System.Data.DbType.DateTime).Value = createDate;
  73:             cmd.Parameters.Add("FailedPasswordAttemptCount", System.Data.DbType.Int32).Value = 0;
  74:             cmd.Parameters.Add("FailedPasswordAttemptWindowStart", System.Data.DbType.DateTime).Value = createDate;
  75:             cmd.Parameters.Add("FailedPasswordAnswerAttemptCount", System.Data.DbType.Int32).Value = 0;
  76:             cmd.Parameters.Add("FailedPasswordAnswerAttemptWindowStart", System.Data.DbType.DateTime).Value = createDate;
  77:  
  78:             try
  79:             {
  80:                 conn.Open();
  81:  
  82:                 int recAdded = cmd.ExecuteNonQuery();
  83:  
  84:                 if (recAdded > 0)
  85:                 {
  86:                     status = MembershipCreateStatus.Success;
  87:                 }
  88:                 else
  89:                 {
  90:                     status = MembershipCreateStatus.UserRejected;
  91:                 }
  92:             }
  93:             catch (AdsException e)
  94:             {
  95:                 if (WriteExceptionsToEventLog)
  96:                 {
  97:                     WriteToEventLog(e, "CreateUser");
  98:                 }
  99:  
 100:                 status = MembershipCreateStatus.ProviderError;
 101:             }
 102:             finally
 103:             {
 104:                 conn.Close();
 105:             }
 106:  
 107:  
 108:             return GetUser(username, false);
 109:         }
 110:         else
 111:         {
 112:             status = MembershipCreateStatus.DuplicateUserName;
 113:         }
 114:  
 115:  
 116:         return null;
 117:     }

Users are updated in a similar fashion using the UpdateUser method. The user is found by providing a MembershipUser object to the function. You can obtain the MembershipUser object by using the one of the GetUser functions. Other helper functions also write to the user record. For example the LastLoginDate is updated by the ValidateUser function when a user successfully logs into the system. Alternately the FailedPasswordAttemptCount gets incremented along with the FailedPasswordAttemptWindowStart if the password is invalid.

The following code demonstrates the implementation of the GetUser public method. The user information is read from the database and and loaded into a DataReader object. A helper function, GetUserFromReader,  inserts the user information into a MembershipUser object which is then returned. The method also updates the LastActivityDate in the database.

   1: public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
   2: {
   3:     AdsConnection conn = new AdsConnection(connectionString);
   4:     AdsCommand cmd = new AdsCommand("SELECT UserID, Username, Email, PasswordQuestion," +
   5:           " Comment, IsApproved, IsLockedOut, CreationDate, LastLoginDate," +
   6:           " LastActivityDate, LastPasswordChangedDate, LastLockedOutDate" +
   7:           " FROM Membership WHERE UserID = :UserID", conn);
   8:  
   9:     cmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey;
  10:  
  11:     MembershipUser u = null;
  12:     AdsDataReader reader = null;
  13:  
  14:     try
  15:     {
  16:         conn.Open();
  17:  
  18:         reader = cmd.ExecuteReader();
  19:  
  20:         if (reader.HasRows)
  21:         {
  22:             reader.Read();
  23:             u = GetUserFromReader(reader);
  24:  
  25:             if (userIsOnline)
  26:             {
  27:                 AdsCommand updateCmd = new AdsCommand("UPDATE Membership " +
  28:                           "SET LastActivityDate = :LastActivityDate " +
  29:                           "WHERE UserID = :UserID", conn);
  30:  
  31:                 updateCmd.Parameters.Add("LastActivityDate", System.Data.DbType.DateTime).Value = DateTime.Now;
  32:                 updateCmd.Parameters.Add("UserID", System.Data.DbType.String).Value = providerUserKey;
  33:  
  34:                 updateCmd.ExecuteNonQuery();
  35:             }
  36:         }
  37:  
  38:     }
  39:     catch (AdsException e)
  40:     {
  41:         if (WriteExceptionsToEventLog)
  42:         {
  43:             WriteToEventLog(e, "GetUser(Object, Boolean)");
  44:  
  45:             throw new ProviderException(exceptionMessage);
  46:         }
  47:         else
  48:         {
  49:             throw e;
  50:         }
  51:     }
  52:     finally
  53:     {
  54:         if (reader != null) { reader.Close(); }
  55:  
  56:         conn.Close();
  57:     }
  58:  
  59:     return u;
  60: }
   1: private MembershipUser GetUserFromReader(AdsDataReader reader)
   2: {
   3:     object providerUserKey = reader.GetValue(0);
   4:     string username = reader.GetString(1);
   5:     string email = reader.GetString(2);
   6:  
   7:     string passwordQuestion = "";
   8:     if (reader.GetValue(3) != DBNull.Value)
   9:         passwordQuestion = reader.GetString(3);
  10:  
  11:     string comment = "";
  12:     if (reader.GetValue(4) != DBNull.Value)
  13:         comment = reader.GetString(4);
  14:  
  15:     bool isApproved = reader.GetBoolean(5);
  16:     bool isLockedOut = reader.GetBoolean(6);
  17:     DateTime creationDate = reader.GetDateTime(7);
  18:  
  19:     DateTime lastLoginDate = new DateTime();
  20:     if (reader.GetValue(8) != DBNull.Value)
  21:         lastLoginDate = reader.GetDateTime(8);
  22:  
  23:     DateTime lastActivityDate = reader.GetDateTime(9);
  24:     DateTime lastPasswordChangedDate = reader.GetDateTime(10);
  25:  
  26:     DateTime lastLockedOutDate = new DateTime();
  27:     if (reader.GetValue(11) != DBNull.Value)
  28:         lastLockedOutDate = reader.GetDateTime(11);
  29:  
  30:     MembershipUser u = new MembershipUser(this.Name,
  31:                                           username,
  32:                                           providerUserKey,
  33:                                           email,
  34:                                           passwordQuestion,
  35:                                           comment,
  36:                                           isApproved,
  37:                                           isLockedOut,
  38:                                           creationDate,
  39:                                           lastLoginDate,
  40:                                           lastActivityDate,
  41:                                           lastPasswordChangedDate,
  42:                                           lastLockedOutDate);
  43:  
  44:     return u;
  45: }

The ASP.NET Login Controls interact with the provider to obtain user information. For example; the LoginControl uses the  ValidateUser method to verify that the correct username and password are correct. The CreateUserWizard uses the CreateUser event to create a new user and so on.

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 Role Provider next.