Role Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Role providers provide the interface between ASP.NET's role management service (the "role manager") and role data sources. The two most common reasons for writing a custom role provider are:

  • You wish to store role information in a data source that is not supported by the role providers included with the .NET Framework, such as an Oracle database or a Web service.
  • You wish to store role information in a SQL Server database whose schema differs from that of the database used by System.Web.Security.SqlRoleProvider-if, for example, you need to integrate ASP.NET's role manager with an existing role database.

The fundamental job of a role provider is to interface with data sources containing containing role data mapping users to roles, and to provide methods for creating roles, deleting roles, adding users to roles, and so on. Given a user name, the role manager relies on the role provider to determine whether what role or roles the user belongs to. The role manager also implements admninistrative methods such as Roles.CreateRole and Roles.AddUserToRole by calling the underlying methods in the provider.

The RoleProvider Class

Developers writing custom role providers begin by deriving from System.Web.Security.RoleProvider, which derives from ProviderBase and adds abstract methods and properties defining the basic characteristics of a role provider. RoleProvider is prototyped as follows:

public abstract class RoleProvider : ProviderBase
{
    // Abstract properties
    public abstract string ApplicationName { get; set; }

    // Abstract methods
    public abstract bool IsUserInRole (string username,
        string roleName);
    public abstract string[] GetRolesForUser (string username);
    public abstract void CreateRole (string roleName);
    public abstract bool DeleteRole (string roleName,
        bool throwOnPopulatedRole);
    public abstract bool RoleExists (string roleName);
    public abstract void AddUsersToRoles (string[] usernames,
        string[] roleNames);
    public abstract void RemoveUsersFromRoles (string[] usernames,
        string[] roleNames);
    public abstract string[] GetUsersInRole (string roleName);
    public abstract string[] GetAllRoles ();
    public abstract string[] FindUsersInRole (string roleName,
        string usernameToMatch);
}

The following table describes RoleProvider's members and provides helpful notes regarding their implementation. Unless otherwise noted, RoleProvider methods that accept user names, role names, and other strings as input consider null (Nothing in Visual Basic) or empty strings to be errors and throw ArgumentNullExceptions or ArgumentExceptions in response.

Method or Property Description
ApplicationName The name of the application using the role provider. ApplicationName is used to scope role data so that applications can choose whether to share role data with other applications. This property can be read and written.
IsUserInRole Takes, as input, a user name and a role name and determines whether the specified user is associated with the specified role.

If the user or role does not exist, IsUserInRole throws a ProviderException.

GetRolesForUser Takes, as input, a user name and returns the names of the roles to which the user belongs.

If the user is not assigned to any roles, GetRolesForUser returns an empty string array (a string array with no elements). If the user name does not exist, GetRolesForUser throws a ProviderException.

CreateRole Takes, as input, a role name and creates the specified role.

CreateRole throws a ProviderException if the role already exists, the role name contains a comma, or the role name exceeds the maximum length allowed by the data source.

DeleteRole Takes, as input, a role name and a Boolean value that indicates whether to throw an exception if there are users currently associated with the role, and then deletes the specified role.

If the throwOnPopulatedRole input parameter is true and the specified role has one or more members, DeleteRole throws a ProviderException and does not delete the role. If throwOnPopulatedRole is false, DeleteRole deletes the role whether it is empty or not.

When DeleteRole deletes a role and there are users assigned to that role, it also removes users from the role.

RoleExists Takes, as input, a role name and determines whether the role exists.
AddUsersToRoles Takes, as input, a list of user names and a list of role names and adds the specified users to the specified roles.

AddUsersToRoles throws a ProviderException if any of the user names or role names do not exist. If any user name or role name is null (Nothing in Visual Basic), AddUsersToRoles throws an ArgumentNullException. If any user name or role name is an empty string, AddUsersToRoles throws an ArgumentException.

RemoveUsersFromRoles Takes, as input, a list of user names and a list of role names and removes the specified users from the specified roles.

RemoveUsersFromRoles throws a ProviderException if any of the users or roles do not exist, or if any user specified in the call does not belong to the role from which he or she is being removed.

GetUsersInRole Takes, as input, a role name and returns the names of all users assigned to that role.

If no users are associated with the specified role, GetUserInRole returns an empty string array (a string array with no elements). If the role does not exist, GetUsersInRole throws a ProviderException.

GetAllRoles Returns the names of all existing roles. If no roles exist, GetAllRoles returns an empty string array (a string array with no elements).
FindUsersInRole Takes, as input, a search pattern and a role name and returns a list of users belonging to the specified role whose user names match the pattern. Wildcard syntax is data-source-dependent and may vary from provider to provider. User names are returned in alphabetical order.

If the search finds no matches, FindUsersInRole returns an empty string array (a string array with no elements). If the role does not exist, FindUsersInRole throws a ProviderException.

Your job in implementing a custom role provider in a derived class is to override and provide implementations of RoleProvider's abstract members, and optionally to override key virtuals such as Initialize.

Scoping of Role Data

All role providers inherit from RoleProvider a property named ApplicationName whose purpose it to scope the data managed by the provider. Applications that specify the same ApplicationName when configuring the role provider share role data; applications that specify unique ApplicationNames do not. Role provider implementations must therefore associate role names with application names so operations performed on role data sources can be scoped accordingly.

As an example, a role provider that stores role data in a SQL database might use a command similar to the following to delete the role named "Administrators" from the application named "Contoso:"

DELETE FROM Roles
WHERE Role='Administrators' AND ApplicationName='Contoso'

The AND in the WHERE clause ensures that other applications containing roles named "Administrators" are not affected.

ReadOnlyXmlRoleProvider

The code below contains the source code for a rudimentary role provider named ReadOnlyXmlRoleProvider-the counterpart to the ReadOnlyXmlMembershipProvider class presented in Membership Providers. ReadOnlyXmlRoleProvider supports applications that use the ASP.NET role manager to restrict access to resources based on role memberships. It implements RoleProvider methods that read from the data source, but it doesn't implement methods that write to the data source. Roles methods such as IsUserInRole and RoleExists will work when ReadOnlyXmlRoleProvider is acting as the role provider; methods such as CreateRole and AddUserToRole will not.

ReadOnlyXmlRoleProvider

using System;
using System.Web.Security;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Hosting;
using System.Xml;
using System.Security.Permissions;
using System.Web;

public class ReadOnlyXmlRoleProvider : RoleProvider
{
    private Dictionary<string, string[]> _UsersAndRoles =
        new Dictionary<string, string[]>(16,
        StringComparer.InvariantCultureIgnoreCase);

    private Dictionary<string, string[]> _RolesAndUsers =
        new Dictionary<string, string[]>(16,
        StringComparer.InvariantCultureIgnoreCase);

    private string _XmlFileName;

    // RoleProvider properties
    public override string ApplicationName
    {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }

    // RoleProvider methods
    public override void Initialize(string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

        // Assign the provider a default name if it doesn't have one
        if (String.IsNullOrEmpty(name))
            name = "ReadOnlyXmlRoleProvider";

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description", "Read-only XML role provider");
        }

        // Call the base class's Initialize method
        base.Initialize(name, config);

        // Initialize _XmlFileName and make sure the path
        // is app-relative
        string path = config["xmlFileName"];

        if (String.IsNullOrEmpty (path))
            path = "~/App_Data/Users.xml";

        if (!VirtualPathUtility.IsAppRelative(path))
            throw new ArgumentException
                ("xmlFileName must be app-relative");

        string fullyQualifiedPath = VirtualPathUtility.Combine
            (VirtualPathUtility.AppendTrailingSlash
            (HttpRuntime.AppDomainAppVirtualPath), path);

        _XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath);
        config.Remove ("xmlFileName");

        // Make sure we have permission to read the XML data source and
        // throw an exception if we don't
        FileIOPermission permission =
            new FileIOPermission(FileIOPermissionAccess.Read,
            _XmlFileName);
        permission.Demand();

        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        {
            string attr = config.GetKey(0);
            if (!String.IsNullOrEmpty(attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }

        // Read the role data source. NOTE: Unlike
        // ReadOnlyXmlMembershipProvider, this provider can
        // read the data source at this point because Read-
        // RoleDataStore doesn't call into the role manager
        ReadRoleDataStore();
    }

    public override bool IsUserInRole(string username, string roleName)
    {
        // Validate input parameters
        if (username == null || roleName == null)
            throw new ArgumentNullException();
        if (username == String.Empty || roleName == string.Empty)
            throw new ArgumentException();

        // Make sure the user name and role name are valid
        if (!_UsersAndRoles.ContainsKey (username))
            throw new ProviderException("Invalid user name");
        if (!_RolesAndUsers.ContainsKey(roleName))
            throw new ProviderException("Invalid role name");

        // Determine whether the user is in the specified role    
        string[] roles = _UsersAndRoles[username];
        foreach (string role in roles)
        {
            if (String.Compare(role, roleName, true) == 0)
                return true;
        }

        return false;
    }

    public override string[] GetRolesForUser(string username)
    {
        // Validate input parameters
        if (username == null)
            throw new ArgumentNullException();
        if (username == string.Empty)
            throw new ArgumentException();

        // Make sure the user name is valid
        string[] roles;
        if (!_UsersAndRoles.TryGetValue(username, out roles))
            throw new ProviderException("Invalid user name");

        // Return role names
        return roles;        
    }

    public override string[] GetUsersInRole(string roleName)
    {
        // Validate input parameters
        if (roleName == null)
            throw new ArgumentNullException();
        if (roleName == string.Empty)
            throw new ArgumentException();

        // Make sure the role name is valid
        string[] users;
        if (!_RolesAndUsers.TryGetValue (roleName, out users))
            throw new ProviderException("Invalid role name");

        // Return user names
        return users;        
    }

    public override string[] GetAllRoles()
    {
        int i = 0;
        string[] roles = new string[_RolesAndUsers.Count];
        foreach (KeyValuePair<string, string[]> pair in _RolesAndUsers)
            roles[i++] = pair.Key;
        return roles;
    }

    public override bool RoleExists(string roleName)
    {
        // Validate input parameters
        if (roleName == null)
            throw new ArgumentNullException();
        if (roleName == string.Empty)
            throw new ArgumentException();

        // Determine whether the role exists
        return _RolesAndUsers.ContainsKey(roleName);
    }

    public override void CreateRole(string roleName)
    {
        throw new NotSupportedException();
    }

    public override bool DeleteRole(string roleName,
        bool throwOnPopulatedRole)
    {
        throw new NotSupportedException();
    }

    public override void AddUsersToRoles(string[] usernames,
        string[] roleNames)
    {
        throw new NotSupportedException();
    }

    public override string[] FindUsersInRole(string roleName,
        string usernameToMatch)
    {
        throw new NotSupportedException();
    }

    public override void RemoveUsersFromRoles(string[] usernames,
        string[] roleNames)
    {
        throw new NotSupportedException();
    }

    // Helper method
    private void ReadRoleDataStore()
    {
        XmlDocument doc = new XmlDocument();
        doc.Load(_XmlFileName);
        XmlNodeList nodes = doc.GetElementsByTagName("User");

        foreach (XmlNode node in nodes)
        {
            if (node["UserName"] == null)
                throw new ProviderException
                    ("Missing UserName element");
            
            string user = node["UserName"].InnerText;
            if (String.IsNullOrEmpty(user))
                throw new ProviderException("Empty UserName element");

            if (node["Roles"] == null ||
                String.IsNullOrEmpty (node["Roles"].InnerText))
                _UsersAndRoles.Add(user, new string[0]);
            else
            {
                string[] roles = node["Roles"].InnerText.Split(',');

                // Add the role names to _UsersAndRoles and
                // key them by user name
                _UsersAndRoles.Add(user, roles);

                foreach (string role in roles)
                {
                    // Add the user name to _RolesAndUsers and
                    // key it by role names
                    string[] users1;

                    if (_RolesAndUsers.TryGetValue(role, out users1))
                    {
                        string[] users2 =
                            new string[users1.Length + 1];
                        users1.CopyTo(users2, 0);
                        users2[users1.Length] = user;
                        _RolesAndUsers.Remove(role);
                        _RolesAndUsers.Add(role, users2);
                    }
                    else
                        _RolesAndUsers.Add(role,
                            new string[] { user });
                }
            }
        }
    }
}

ReadOnlyXmlRoleProvider uses an XML file with a schema matching that of the file below as its data source. Each <User> element defines one user, and subelements define the user name and role or roles to which the user belongs. To avoid redundant file I/O and XML parsing, the provider reads the XML file once and populates two private fields with role data:

  • A Dictionary named _UsersAndRoles, which stores lists of roles keyed by user names. Methods such as IsUserInRole and GetRolesForUser use this field to quickly perform role lookups given a user name.
  • A Dictionary named _RolesAndUsers, which stores lists of users keyed by role names. Methods such as GetUsersInRole use this field to quickly perform user name lookups given a role name. GetAllRoles uses it to enumerate role names.

Both fields are populated when the Initialize method calls ReadRoleDataStore. Because the initialization code doesn't use the roles API, there's no danger of Initialize being called recursively. Furthermore, since Initialize is guaranteed to be called on only one thread, ReadRoleDataStore contains no thread synchronization code.

Sample ReadOnlyXmlRoleProvider Data Source

<Users>
  <User>
    <UserName>Bob</UserName>
    <Roles>Members</Roles>
  </User>
  <User>
    <UserName>Alice</UserName>
    <Roles>Members,Administrators</Roles>
  </User>
</Users>

ReadOnlyXmlRoleProvider supports one custom configuration attribute: xmlFileName. The provider's Initialize method initializes a private field named _XmlFileName with the attribute value and defaults to Roles.xml if the attribute isn't present. The Web.config file below that registers ReadOnlyXmlRoleProvider, makes it the default role provider, and designates ~/App_Data/UserRoles.xml as the data source. The type name specified in the <add> element assumes that the provider is deployed in an assembly named CustomProviders.

Web.config file making ReadOnlyXmlRoleProvider the default role provider

<configuration>
  <system.web>
    <roleManager enabled="true"
      defaultProvider="AspNetReadOnlyXmlRoleProvider">
      <providers>
        <add name="AspNetReadOnlyXmlRoleProvider"
          type="ReadOnlyXmlRoleProvider, CustomProviders"
          description="Read-only XML role provider"
          xmlFileName="~/App_Data/UserRoles.xml"
        />
      </providers>
    </roleManager>
  </system.web>
</configuration>

For simplicity, ReadOnlyXmlRoleProvider doesn't scope role data using the ApplicationName property. Instead, it assumes that different applications will target different role data sources by specifying different XML file names.

Click here to continue on to part 3, Site Map Providers

© Microsoft Corporation. All rights reserved.