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 role containing 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 mustoverride methods and properties defining the basic characteristics of a role provider. RoleProvider is prototyped as follows:

Public MustInherit Class RoleProvider 
Inherits ProviderBase
   
   ' Abstract properties
   Public MustOverride Property ApplicationName() As String

   ' Abstract methods
   Public MustOverride Function IsUserInRole( _
ByVal username As String, _
ByVal roleName As String) As Boolean
   Public MustOverride Function GetRolesForUser( _
ByVal username As String) As String()
   Public MustOverride Sub CreateRole(ByVal roleName As String)
   Public MustOverride Function DeleteRole( _
ByVal roleName As String, 
ByVal throwOnPopulatedRole As Boolean) As Boolean
   Public MustOverride Function RoleExists( _
ByVal roleName As String) As Boolean
   Public MustOverride Sub AddUsersToRoles( _
ByVal usernames As String(), ByVal roleNames As String())
   Public MustOverride Sub RemoveUsersFromRoles( _
ByVal usernames As String(), ByVal roleNames As String())
   Public MustOverride Function GetUsersInRole( _
ByVal roleName As String) As String()
   Public MustOverride Function GetAllRoles() As String()
   Public MustOverride Function FindUsersInRole( _
ByVal roleName As String, _
ByVal usernameToMatch As String) As String()
End Class

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 Nothing or empty strings to be errors and throw ArgumentNullExceptions or ArgumentExceptions in response.

Method or PropertyDescription
ApplicationNameThe 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.
IsUserInRoleTakes, 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.

GetRolesForUserTakes, 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.

CreateRoleTakes, 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.

DeleteRoleTakes, 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.

RoleExistsTakes, as input, a role name and determines whether the role exists.
AddUsersToRolesTakes, 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 Nothing, AddUsersToRoles throws an ArgumentNullException. If any user name or role name is an empty string, AddUsersToRoles throws an ArgumentException.

RemoveUsersFromRolesTakes, 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.

GetUsersInRoleTakes, 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.

GetAllRolesReturns the names of all existing roles. If no roles exist, GetAllRoles returns an empty string array (a string array with no elements).
FindUsersInRoleTakes, 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 mustoverride members, and optionally to override key overridable 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

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

Public Class ReadOnlyXmlRoleProvider 
Inherits RoleProvider

    Private _UsersAndRoles As Dictionary(Of String, String()) = _
            New Dictionary(Of String, String()) _
            (16, StringComparer.InvariantCultureIgnoreCase)

    Private _RolesAndUsers As Dictionary(Of String, String()) = _
            New Dictionary(Of String, String()) _
            (16, StringComparer.InvariantCultureIgnoreCase)

    Private _XmlFileName As String

    ' RoleProvider properties
    Public Overrides Property ApplicationName() As String
        Get
            Throw New NotSupportedException()
        End Get
        Set(ByVal value As String)
            Throw New NotSupportedException()
        End Set
    End Property

    ' RoleProvider methods
    Public Overrides Sub Initialize(ByVal name As String, _
                            ByVal config As NameValueCollection)
        ' Verify that config isn't null
        If config Is Nothing Then
            Throw New ArgumentNullException("config")
        End If

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

        ' Add a default "description" attribute to config if the
        ' attribute doesn't exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", "Read-only XML role provider")
        End If

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

        ' Initialize _XmlFileName and make sure the path
        ' is app-relative
        Dim path As String = config("xmlFileName")

        If String.IsNullOrEmpty(path) Then
            path = "~/App_Data/Users.xml"
        End If

        If (Not VirtualPathUtility.IsAppRelative(path)) Then
            Throw New _
                ArgumentException("xmlFileName must be app-relative")
        End If

        Dim fullyQualifiedPath As String = _
        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
        Dim permission As FileIOPermission = _
        New FileIOPermission(FileIOPermissionAccess.Read, _XmlFileName)

        permission.Demand()

        ' Throw an exception if unrecognized attributes remain
        If config.Count > 0 Then
            Dim attr As String = config.GetKey(0)
            If (Not String.IsNullOrEmpty(attr)) Then
                Throw New _
                ProviderException("Unrecognized attribute: " & attr)
            End If
        End If

        ' 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()
    End Sub

    Public Overrides Function IsUserInRole(ByVal username As String, _
                                ByVal roleName As String) As Boolean

        ' Validate input parameters
        If username Is Nothing OrElse roleName Is Nothing Then
            Throw New ArgumentNullException()
        End If
        If username = String.Empty OrElse roleName = String.Empty Then
            Throw New ArgumentException()
        End If

        ' Make sure the user name and role name are valid
        If (Not _UsersAndRoles.ContainsKey(username)) Then
            Throw New ProviderException("Invalid user name")
        End If
        If (Not _RolesAndUsers.ContainsKey(roleName)) Then
            Throw New ProviderException("Invalid role name")
        End If

        ' Determine whether the user is in the specified role    
        Dim roles As String() = _UsersAndRoles(username)
        For Each role As String In roles
            If String.Compare(role, roleName, True) = 0 Then
                Return True
            End If
        Next role

        Return False
    End Function

    Public Overrides Function GetRolesForUser( _
                                ByVal username As String) As String()

        ' Validate input parameters
        If username Is Nothing Then
            Throw New ArgumentNullException()
        End If
        If username = String.Empty Then
            Throw New ArgumentException()
        End If

        ' Make sure the user name is valid
        Dim roles As String() = Nothing
        If (Not _UsersAndRoles.TryGetValue(username, roles)) Then
            Throw New ProviderException("Invalid user name")
        End If

        ' Return role names
        Return roles
    End Function

    Public Overrides Function GetUsersInRole( _
                                ByVal roleName As String) As String()

        ' Validate input parameters
        If roleName Is Nothing Then
            Throw New ArgumentNullException()
        End If
        If roleName = String.Empty Then
            Throw New ArgumentException()
        End If

        ' Make sure the role name is valid
        Dim users As String() = Nothing
        If (Not _RolesAndUsers.TryGetValue(roleName, users)) Then
            Throw New ProviderException("Invalid role name")
        End If

        ' Return user names
        Return users
    End Function

    Public Overrides Function GetAllRoles() As String()
        Dim i As Integer = 0
        Dim roles As String() = New String(_RolesAndUsers.Count - 1) {}
        For Each pair As KeyValuePair(Of String, String()) _
            In _RolesAndUsers
            roles(i) = pair.Key
            i = i + 1
        Next
        Return roles
    End Function

    Public Overrides Function RoleExists( _
                            ByVal roleName As String) As Boolean
        ' Validate input parameters
        If roleName Is Nothing Then
            Throw New ArgumentNullException()
        End If
        If roleName = String.Empty Then
            Throw New ArgumentException()
        End If

        ' Determine whether the role exists
        Return _RolesAndUsers.ContainsKey(roleName)
    End Function

    Public Overrides Sub CreateRole(ByVal roleName As String)
        Throw New NotSupportedException()
    End Sub

    Public Overrides Function DeleteRole(ByVal roleName As String, _
                    ByVal throwOnPopulatedRole As Boolean) As Boolean
        Throw New NotSupportedException()
    End Function

    Public Overrides Sub AddUsersToRoles( _
            ByVal usernames As String(), ByVal roleNames As String())
        Throw New NotSupportedException()
    End Sub

    Public Overrides Function FindUsersInRole( _
            ByVal roleName As String, _
            ByVal usernameToMatch As String) As String()
        Throw New NotSupportedException()
    End Function

    Public Overrides Sub RemoveUsersFromRoles( _
            ByVal usernames As String(), ByVal roleNames As String())
        Throw New NotSupportedException()
    End Sub

    ' Helper method
    Private Sub ReadRoleDataStore()
        Dim doc As XmlDocument = New XmlDocument()
        doc.Load(_XmlFileName)
        Dim nodes As XmlNodeList = doc.GetElementsByTagName("User")

        For Each node As XmlNode In nodes
            If node("UserName") Is Nothing Then
                Throw New ProviderException("Missing UserName element")
            End If

            Dim user As String = node("UserName").InnerText
            If String.IsNullOrEmpty(user) Then
                Throw New ProviderException("Empty UserName element")
            End If

            If node("Roles") Is Nothing OrElse _
                    String.IsNullOrEmpty(node("Roles").InnerText) Then
                _UsersAndRoles.Add(user, New String() {})
            Else
                Dim roles As String() = _
                            node("Roles").InnerText.Split(","c)

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

                For Each role As String In roles
                    ' Add the user name to _RolesAndUsers and
                    ' key it by role names
                    Dim users1 As String() = Nothing

                    If _RolesAndUsers.TryGetValue(role, users1) Then
                        Dim users2 As String() = _
                                        New String(users1.Length) {}
                        users1.CopyTo(users2, 0)
                        users2(users1.Length) = user
                        _RolesAndUsers.Remove(role)
                        _RolesAndUsers.Add(role, users2)
                    Else
                        _RolesAndUsers.Add(role, New String() {user})
                    End If
                Next role
            End If
        Next node
    End Sub

End Class

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.

Show: