Role-Based Security

Summary

Introduction

Roles (a group of user that have the same security permissions) are often used in applications to enforce levels of security policy, or when multiple approvals are required to perform an operation. An example of the first case is when a user in the role of a clerk can authorize loans only up to $X, but a user in the role of a manager can authorize loans up to $10X. An example of the second case is when a user in the role of an employee can only submit a purchase request, but a user in the role of a director can approve or even reject the request.

.NET Framework role-based security supports authorization making information about the principal (identity and role of the user) available to the current thread. Authorization can then be obtained based on the user's role and/or identity. A role is a named-set of principals that share the same security privileges. A principal can be a member of one or more roles.

.NET Framework implements role-based security by supplying PrincipalPermission objects. A PrincipalPermission class represents the identity or the role that a principal must match.

Principal and Identity Objects

You can discover the role or identity of a user through the Principal object. The Principal object contains a reference to an identity object which encapsulates information about the user or the entity being validated. Identity classes must implement the IIDentity interface. The .NET Framework contains a few identity classes such as GeneircIdentity that can be used for most logon scenarios, and WindowsIdentity class that can be used when you want you application to rely on Windows authentication.

The Principal object represents the security context under which the code is running. Applications that implement role-based security grant rights based on the role associated with a Principal Object.  Similar to identity objects, .NET Framework principal classes must implement the IPrincipal interface. The .NET Framework contains two principal classes such as GenericPrincipal that represents a generic principal, and WindowsPrincipal that allows code to check which Windows group the associated user belongs to.

A Principal object is bound to a CallContext object within an application domain.  A default call context object is always created with new AppDomain, so there is always a CallContext object to accept the Principal object. When a new thread is created, a new CallContext object is also created for the thread and the Principal object reference is automatically copied from the creating thread to the created thread.

Creating WindowsIdentity and WindowsPrincipal Objects

The WindowsIdentity object encapsulates information about Windows accounts. Use this object if you want to make authorization decisions based on a user's Windows account information. For example, using WindowsIdentity and WindowsPrincipal objects, you can write an application that requires all users to be currently validated by a Windows NT or 2000 domain. Or you can allow certain domain accounts to access your application while denying others.

There are ways to create a WindowsPrincipal object depending on whether code must repeatedly perform role-based validation or only needs to do it once:

Creating WindowsPrincipal objects for repeated validation

// C#

// Using SetPrinciaplPolicy specify how principal and identity objects should be attached to a thread

AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal );

// Now retrieve the principal that encapsulates the current Windows user
WindowsPrincipal prnpl = (WindowsPrincipal)Thread.CurrentPrincipal;

// Access the principal properties ... 

Creating WindowsPrincipal object for a single validation

// C#

// Initialize a new WindowsIdentity object

WindowsIdentity id = WindowsIdentity.GetCurrent();

// Create a new principal object passing it the new identity object
WindowsPrincipal prnpl = new WindowsPrincipal(id );

// Access the principal properties ... 

Creating GenericPrincipal and GenericIdentity Objects

GenericIdentity and GenericPrincipal objects can be used to create an authentication scheme that exists independent of Windows NT/2000 domains. For example, an application can use these objects to prompt the user for a password and login, verify this information against some database entry, then create identity and principal objects based on these values. To create an instance of a GenericPrincipal class:

// Create a generic identity object and initialize it with a user name/account
GenericIdentity id = new GenericIdentity( "yazan" );

// Create a generic principal object and initialize it with the generic identity object and an array
// of strings that represent roles you want associated with this account

String[] roles = {"Directory", "Vice President" ); 
GenericPrincipal prnpl = new GenericPrincipal( id, roles );

// Finally attach the new principal object to the current thread
Thread.CurrentPrincipal = prnpl

// Access the principal properties ... 

Replacing the Principal Object

Applications that provide authentication services must be able to replace the current principal object for a given thread. Further, the security system must be able to protect the ability to replace principal objects because a maliciously attached principal object can severely compromise the security of the application by claiming an untrue identity. Therefore, applications that require the ability to replace Principal objects must be granted the System.Security.Permissions.SecurityPermission object for principal control

To replace the current Principal object:

  1. Create the replacement Principal and the associated Identity objects.
  2. Create a new System.Security.Permissions.SecurityPermission object passing the constructor the SecurityPermissionAttribute.ControlPrincipal enumeration value.
  3. Attach the new Principal object to the CallContext object ( Thread.CurrentPrincipal = MyNewPrincipalObject; )

Impersonating and Reverting

Sometimes an account may have to act on behalf of several users, performing functions that relate to the role of the user. For example, an ASP.NET application might accept a normal user's token from IIS in order to perform some operation on behalf of that user and then revert to itself again. Next, it accepts an administrator's token from IIS in order to perform some privileged operations on and then revert to itself again.

In situations where the application must impersonate a Windows account that has not been attached to the current thread by IIS, you must retrieve that account's token and use to activate that account. This impersonating and reverting is accomplished as follows:

// Retrieve an account token for a particular user (perhaps by making a call to the unmanaged LogonUser API)
...

// Create a new instance of WindowsIdentity passing the token acquired above
WindowsIdentity idImpersonated = new WindowsIdentity( hToken );

// Begin impersonation by creating an intance of WindowsImpersonationContext
WindowsImpersonationContext ctx = idImpersonated.Impersonate();

// When you're done, revert the impersonation
ctx.Undo()

PrincipalPermission Objects

A PrincipalPermission object represents the identity and the role that a particular principal a class must have to run. A PrincipalPermission object can be used for both declarative and imperative security checks.

To use PrincipalPermission class imperatively, create a new instance of PrincipalPermission class and pass in the name and the role you want users to have in order to access your code:

// C#: Impertatively
PrincipalPermission obPP = new PrincipalPermission( "Yazan", "Developer" );

// C#: Declaratively
[PrincipalPermissionAttribute(SecurityAction.Demand, Name="Yazan", Role="Developer")]
...

When the security check is performed, both the identity and the role must match for the check to succeed. Note that passing null for the identity indicates that the identity of the principal can by anything. Similarly, passing a null for the role means the role of the principal can by any role (or no role).

Combining principal objects makes most since when performing a union operation on two PrincipalPermission objects, especially when you want to compact a set of conditions that you want to test.  For example, a union operation can be used when you want to check that the user is either in role A or role B.  For example, the following checks to see if the principal object represents "Yazan" in the role of  "Developer" and "TeamLeader"

// C#: Impertatively
PrincipalPermission obPP1 = new PrincipalPermission( "Yazan", "Developer" );
PrincipalPermission obPP2 = new PrincipalPermission( "Yazan", "TeamLeader" );
PrincipalPermission obPPUnion = ((PrincipalPermission)obPP1.Union(obPP2)).Demand();

Role-Based Security Checks

Once identity and permission objects have been defined, you can perform security checks against them in the following ways:

Managed code can use the above method to determine whether a particular permission object is a member of some role, has a known identity, or represents a known identity acting in a known role. To cause the security checks to occur using imperative or declarative security, a security demand for an appropriately-constructed PrincipalPermission object must be made. During the security check, the CLR examines the caller's principal object to determine whether its identity and role match those in the demanded PrincipalPermission object. If there was no match, a SecurityException is thrown.

Another approach is that you can access the values of the current principal object directly. In this case, you simple read the values of the current thread's principal user, or use IsInRole to perform authorization.

Performing Imperative Security Checks

An imperative check is useful when many methods or assemblies in the application domain need to make role-based decisions. You can call PrincipalPermission.Demand() to determine whether the current principal object represents the required identity, role, or both:

// C#
MyPrincipalPermission.Demand();

The following example illustrates how to use imperative checks to make a decision against a role-based demand:

/* C#: Use imperative cheks to ensure that a GenericPrincipal object matches the PrincipalPermission object*/
public class MyPrincipalPermission
{
    public MyPrincipalPermission() {}

    public void TestPrincipalPermission()
    {
        // Create generic identity and generic principal objects
        System.Security.Principal.GenericIdentity gid = new GenericIdentity( "yazan", "developer" );
        string[] roles = {"developer", "manager" };
        System.Security.Principal.GenericPrincipal gpn = new GenericPrincipal( gid, roles );

        // Now make this new generic principal the current one
        Thread.CurrentPrincipal = gpn;

        // Now call a method that needs to make checks based on the current principal (which, in this
        // case is 'yazan' in the role of 'developer')
        PerformProtectedOperation1();
        PerformProtectedOperation2();
    }

    private void PerformProtectedOperation1()
    {
        try
        {
            // Now attempt to verify the name and role of the caller via the PrincipalPermission object
            PrincipalPermission pp = new PrincipalPermission("yazan", "developer");
            pp.Demand();

            // If we get here, then the caller passed the security check
            Console.WriteLine( "Security check performed. Access authorized" );
        }
        catch( Exception e)
        {
            Console.WriteLine( e.Message );
        }
    }

    private void PerformProtectedOperation2()
    {
        try
        {
            // Now attempt to verify the name and role of the caller via the PrincipalPermission object
            PrincipalPermission pp = new PrincipalPermission("yazan", "CEO");
            pp.Demand();


            // If we get here, then the caller passed the security check
            Console.WriteLine( "Security check performed. Access authorized" );
        }
        catch( Exception e)
        {
            Console.WriteLine( e.Message );
        }
    }
}

Output from the above code:

Performing Declarative Security Checks

Declarative demands for PrincipalPermission work the same way as declarative demands for code access permissions. Demands can be placed at the class level or at the method, properties, or events level. As in code access permissions, if a declarative demands is placed both at the class and method levels, then the declarative demand on the method overrides that on the class. The following example illustrated a declarative PrincipalPermission

public class MyPrincipalPermission
{
    public MyPrincipalPermission() {}

    public void TestPrincipalPermission()
    {
        // Create generic identity and generic principal objects
        System.Security.Principal.GenericIdentity gid = new GenericIdentity( "yazan", "developer" );
        string[] roles = {"developer", "manager" };
        System.Security.Principal.GenericPrincipal gpn = new GenericPrincipal( gid, roles );

        // Now make this new generic principal the current one
        Thread.CurrentPrincipal = gpn;

        // Now call a method that needs to make checks based on the current principal (which, in this
        // case is 'yazan' in the role of 'developer')
        PerformProtectedOperation3();
    }

    // Attempt to verify declaratively the name and role of the caller
    [PrincipalPermissionAttribute(SecurityAction.Demand, Name="yazan", Role="SupremeCommander")]
    private void PerformProtectedOperation3()
    {
        try
        {
            // If we get here, then the caller passed the security check
            Console.WriteLine( "Security check performed. Access authorized" );
        }
        catch( Exception e)
        {
            Console.WriteLine( e.Message );
        }
    }
}

Note the because PerformProtectedOperation3() declarative specified a different role that the current principal, an exception is thrown when you attempt to call the method. The exception is not thrown inside PerformProtectedOperation3() but rather in the caller function, TestPrincipalPermission() and because there was no try/catch block, Visual Studio.NET reported the exception in the error dialog box.

Accessing the principal object directly

Although using imperative and declarative demands to invoke role-based security checks is the primary mechanism for checking and enforcing identity and role memberships, sometime you may want to access the Principal object and its associated Identity object directly to do authorization tasks without creating a permission objects. You may opt to use this way rather than use declarative or imperative demands, if you do not want a thrown exception to be the default behavior for a failed security check, or if you want to access behaviors that are specific to an application-defined Principal object (i.e., user A can call f1(), user B can call f2(), and user C can call either)

To access the Principal object directly, you can use the static CurrentPrincipal property on the System.Threading.Thread class. After obtaining the principal object, you can then use conditional statements to control access to your code:

WindowsPrincipal prnpl = (WindowsPrincipal)Thread.CurrentPrincipal;
if (prnpl.Identity.Name = "yazan")
{
    // Permit access to code...
}
else
{
    // Deny access to code...
}