Granular Role Based Security Without a Line of Code: Attribute-Based Authorization

 

Irena Kennedy
Microsoft Corporation

March 2006

Applies To:
   Role Based Security
   Application Security
   .NET Application Development
   Enterprise Architecture
   Solution Architecture

Summary: For well over a decade, Windows application developers have been employing role based security in ever increasing numbers. Regardless of the industry, most business systems require means of policy enforcement, and developers have been eagerly using the IsInRole type of logic—disabling or hiding functionality unavailable to the logged on user, based on the known identity, role membership, and the associated permission set. This article details a security implementation that moves the authorization checking from your forms into the "plumbing" layer. While this implementation is based on Microsoft Composite UI Application Block and Microsoft Authorization Manager (AzMan), the usage of either is not required. (20 printed pages)

Contents

Introduction
Make It Simple
Let Me Show You How...
Conclusion
Other Suggested Reading

Introduction

Most applications implement role based security to assure that only authorized users have access to restricted application functionality (features). You know the drill—"only certain users can modify salary information in an HR application," or "only managers can view detailed sales reports." There are also more complex examples of authorization, requiring the usage of business rules, such as "Managers have signing authority of $10,000; anything over that amount requires two signatures."

Data access and functionality restrictions can be implemented at different layers—database, middle/data tier, and user interface. Some require dynamic, context sensitive, business rules; some are simple membership based authorization rules. In this article, I'm going to limit the discussion to the user interface implementation that's based solely on role membership—for example if currently logged in users do not belong to the Managers role, hide the View Report button.

Make It Simple

Today, when authorization is done in the UI tier, most developers accomplish it by sprinkling lots of If-Then or IsInRole statements throughout the code to display proper user interfaces. Inspired by the beauty of the ASP.NET server side controls design that allows controls to render themselves differently based on browser capabilities and other context specific attributes, I decided to apply the same logic to security. Why should application developers write any code instead of letting controls (buttons, grids, text boxes) or other plumbing code set the visible, enabled, read only (and so on) properties themselves, based on permissions for the logged in user discovered at run time?

In the previous example, if a logged in user doesn't have permissions to the EditSalary feature on a form that displays Salary data, the 'Salary' textbox control could automatically be rendered as read-only, based on design time settings, with zero lines of code on the form providing the user interface.

Let Me Show You How...

Note   While this article details an implementation based on Microsoft Composite UI Application Block and Microsoft Authorization Manager (AzMan), one could easily apply the same principals without the usage of CAB and AzMan—just think of the "Secure Controls" library that encapsulates all the rendering logic.

Step 1: Define Restricted Application Functionality and Configure Access

Note   For brevity, I'm omitting the instructions on how to use AzMan.

Using AzMan, define a set of privileged operations that your application will use, and configure who should have access to those "features." For example, if you want to restrict who can edit salary information, you'll need to add an operation EditSalary and give access to that operation to certain roles.

Step 2: Add AuthorizationAttribute Class

This step is simple—just copy and paste the following code to your application code; preferably to some security related assembly. If you're using some sort of framework (Composite UI Application Block, for example), you may choose to add the class there, instead.

C#:

using System;
using System.Collections.Generic;
using System.Text;

namespace YourCompany
{      
   /// <summary>
   /// Applied to controls as an indication that user must possess 
   /// rights to see/act on this control
   /// </summary>
   [AttributeUsage(AttributeTargets.Field, AllowMultiple=false)]
   public class AuthorizationAttribute : System.Attribute
   {
      private string _operationName = null;
      private string _propertyName = null;
      private object _propertyValue = null;

      /// <summary>
      /// Constructor.  
      /// </summary>
      public AuthorizationAttribute(string operationName, 
string propertyName, object propertyValue)
      {
         _operationName = operationName;
         _propertyName = propertyName;
         _propertyValue = propertyValue;
      }

      /// <summary>
      /// OperationName must match action or task in AzMan.
      /// </summary>
      public string OperationName
      {
         get { return _operationName; }
      }

      /// <summary>
      /// Property name, which to invoke if logged in user doesn't 
      /// have rights to operation
      /// </summary>
      public string PropertyName
      {
         get { return _propertyName; }
      }

      /// <summary>
      /// Property value, which to set if logged in user doesn't 
      /// have rights to operation
      /// </summary>
      public object PropertyValue
      {
         get { return _propertyValue; }
      }
   }
}

Visual Basic:

Imports System
Imports System.Collections.Generic
Imports System.Text

Namespace YourCompany
    ' <summary>
    ' Applied to controls as an indication that user must possess 
    ' rights to see/act on this control
    ' </summary>
    <AttributeUsage(AttributeTargets.Field, _
    AllowMultiple:=False)> _
    Public Class AuthorizationAttribute
        Inherits System.Attribute

        Private _operationName As String = Nothing
        Private _propertyName As String = Nothing
        Private _propertyValue As Object = Nothing

        ' <summary>
        ' Constructor.  
        ' </summary>
        Public Sub New(ByVal operationName As String, _
        ByVal propertyName As String, ByVal propertyValue As Object)
            MyBase.New()
            _operationName = operationName
            _propertyName = propertyName
            _propertyValue = propertyValue
        End Sub

        ' <summary>
        ' OperationName must match action or task in AzMan.
        ' </summary>
        Public ReadOnly Property OperationName() As String
            Get
                Return _operationName
            End Get
        End Property

        ' <summary>
        ' Property name, which to invoke if logged in user doesn't
        ' have rights to operation
        ' </summary>
        Public ReadOnly Property PropertyName() As String
            Get
                Return _propertyName
            End Get
        End Property

        ' <summary>
        ' Property value, which to set if logged in user doesn't 
        ' have rights to operation
        ' </summary>
        Public ReadOnly Property PropertyValue() As Object
            Get
                Return _propertyValue
            End Get
        End Property
    End Class
End Namespace

Step 3: Add Security Attributes to UI Controls that Map to Restricted Operations

The next step is to map defined operations to user interface controls that represent functionality affected by the operations. You do this by adding the following code attribute to such UI controls.

C#:

[YourCompany.Authorization(OperationName, PropertyName, 
PropertyValue)]

Visual Basic:

<YourCompany.Authorization(OperationName, PropertyName, _ 
PropertyValue)>

In this example, you would add the following attribute to the text box corresponding to the salary data:

C#:

[YourCompany.Authorization("EditSalary", "ReadOnly", true)]
private System.Windows.Forms.TextBox Salary;

Visual Basic:

<YourCompany.Authorization("EditSalary", "ReadOnly", True)> _
Friend WithEvents Salary As System.Windows.Forms.TextBox

In essence, you're saying that if the currently logged in user doesn't have rights to "edit salary," set the text box Salary's ReadOnly property to true.

Step 4: Inject Security Checking into Control Creation Code

Note The following code uses XML based AzMan configuration. Make sure to change it to ActiveDirectory based implementation, if necessary. Also, caching must be added to optimize performance.

These modifications are intended for the CAB (Composite UI Application Block) assembly. Alternatively, you could create a library of controls (SecureControls.dll) with SecureTextBox, SecureGrid, and so on, and use the following concepts to archive same net-effect by changing control properties in the control constructors.

Modifying any application block's code comes with a drawback; needing to re-insert the changes into the new version when/if Microsoft publishes any updates to the underlying code.

  • Add references to the following assemblies to the Microsoft.Practices.CompositeUI project.

       - Microsoft.Interop.Security.AzRoles
       - System.Windows.Forms
       - System.Configuration
    
  • Add new member variables in the RootWorkItemInitializationStrategy

    class.

    C#:

    C:\Program Files\Microsoft Composite UI App Block\CSharp
            \Source\CompositeUI\BuilderStrategies\
    RootWorkItemInitializationStrategy.cs file
    
    using Microsoft.Interop.Security;
    using System.Xml;
    
    static AzRoles.IAzApplication _azManApp = null;
    static bool _azManCheck = true;
    static XmlDocument _configDoc = new System.Xml.XmlDocument();
    

    Visual Basic:

    C:\Program Files\Microsoft Composite UI App Block\VB
            \Source\CompositeUI\BuilderStrategies\
    RootWorkItemInitializationStrategy.vb file
    
    Imports Microsoft.Interop.Security
    Imports System.Xml
    
    Dim _azManApp As.AzRoles.IAzApplication = Nothing
    Dim _azManCheck As Boolean = true
    Dim _configDoc As XmlDocument = New System.Xml.XmlDocument
    
  • In the same file, in the BuildUp method:

    C#:

    public override object BuildUp(IBuilderContext context, 
    Type typeToBuild, object existing, string idToBuild) 
    

    Visual Basic:

    Public Overrides Function BuildUp(ByVal context As _ 
    IBuilderContext, ByVal typeToBuild As Type, _ 
    ByVal existing As Object, ByVal idToBuild As String) As Object
    

    replace

    C#:

    return base.BuildUp(context, typeToBuild, existing, idToBuild);   
    

    Visual Basic:

    Return MyBase.BuildUp(context, typeToBuild, existing, idToBuild)
    

    with

    C#:

    object result = base.BuildUp(context, typeToBuild, existing, 
    idToBuild);
    CheckSecurity(result, new System.Collections.Generic.List<int>());
    return result;
    

    Visual Basic:

    Dim result As Object = MyBase.BuildUp(context, typeToBuild, _ 
    existing, idToBuild)
    CheckSecurity(result, _ 
    New System.Collections.Generic.List(Of Integer))
    Return result
    
  • In the same RootWorkItemInitializationStrategy class, add the following methods:

    C#:

    using System.Reflection;
    
    private void CheckSecurity(object o, 
        List<int> processedControls)
    {
        // Avoid stack overflow due to properties like Parent 
        // or TopLevelControl         
        if (o == null)
            return;
    
        int hash = o.GetHashCode();
        bool processed = false;
        foreach (int ctrlHash in processedControls)
        {
            if (ctrlHash == hash)
            {
                processed = true;
                break;
            }
        }
    
        if (processed == true)
            return;
    
        processedControls.Add(hash);
    
        System.Reflection.FieldInfo[] fields = 
            o.GetType().GetFields(BindingFlags.Instance | 
            BindingFlags.NonPublic);
    
        foreach (System.Reflection.FieldInfo field in fields)
        {
            object[] attrs = 
                field.GetCustomAttributes(
                typeof(YourCompany.AuthorizationAttribute), false);
            if (attrs.Length > 0)
            {
                // We only allow one attribute (AllowMultiple = false)
                YourCompany.AuthorizationAttribute attr = 
                (YourCompany.AuthorizationAttribute) attrs[0];
    
                try
                {
                    if (HasAccess(field.Name, 
                        attr.OperationName) == false)
                    {
                        object ctrl = field.GetValue(o);
                        PropertyInfo propInfo = 
                            ctrl.GetType().GetProperty(attr.PropertyName);
                        if (propInfo != null)
                        {
                           propInfo.SetValue(ctrl, attr.PropertyValue, null);
                        }
                    }
                }
                catch (Exception ex)
                {
                    // not fatal
                    // TODO: log the error 
                }
            }
    
            if (field.GetType().IsClass)
            {
                object childO = field.GetValue(o);
    
                if (childO is System.ComponentModel.Component)
                {
                    CheckSecurity(childO, processedControls);
                }
                else if (childO is IEnumerable)
                {
                    foreach (object item in (IEnumerable)childO)
                    {
                        if (item is System.ComponentModel.Component)
                        {
                            CheckSecurity(item, processedControls);
                        }
                    }
                }
            }
        }
    }
    
    private bool HasAccess(string controlName, string operationName)
    {
        bool result = false;
    
        try
        {            
            if (_azManCheck == true)
            {
                if (_azManApp == null)
                {
                    // NOTE: alternatively, you could use
                   //     System.Configuration.ConfigurationManager
                   // IMPORTANT: This is a 'demo-quality' 
                   // implementation.  For 'real' production usage, 
                   // add caching, etc.!!!
                    _configDoc.Load(string.Format("{0}{1}.exe.config",
                        System.AppDomain.CurrentDomain.BaseDirectory,
        System.Diagnostics.Process.GetCurrentProcess().ProcessName));
    
                string authorizationStore = 
                    _configDoc.DocumentElement.SelectSingleNode(
                        "applicationSettings/YourCompany." +   
                        "AuthorizationSettings/setting" + 
                        "[@name='AuthorizationStore']").InnerText;
    
                if (authorizationStore == null || 
                    authorizationStore.Length == 0)
                {
                    if (_azManApp == null)
                    {
                         // TODO: Log error
                         // Don't keep doing same...
                        _azManCheck = false;
                    }
                    else
                    {
                        AzRoles.AzAuthorizationStore store = new 
                            AzRoles.AzAuthorizationStoreClass();
                        store.Initialize(0, 
                            string.Format(@"msxml://{0}{1}",
                            System.AppDomain.CurrentDomain.BaseDirectory, 
                            authorizationStore), null);
    
                        string appName = 
       _configDoc.DocumentElement.SelectSingleNode(
            "applicationSettings/YourCompany.AuthorizationSettings" +
            "/setting[@name='AzManApplicationName']").InnerText;
    
                       _azManApp = store.OpenApplication(appName, null);
    
                       if (_azManApp == null)
                       {
                          // TODO: Log error
                          // Don't keep doing same...
                         _azManCheck = false;
                       }
                    }
                }
            }
    
            if (_azManApp != null)
            {
                // Map operation name to id
                string opId = 
                    configDoc.DocumentElement.SelectSingleNode(
                        string.Format("applicationSettings/YourCompany." + 
                            "AuthorizationSettings/setting[@name='{0}']",
                        operationName)).InnerText;
    
                if (opId == null || opId.ToString().Length == 0)
                {
                    // TODO: Log error   
                }
                else
                {
                    System.Security.Principal.WindowsIdentity id = 
                       (System.Security.Principal.WindowsIdentity) 
                       System.Security.Principal.WindowsIdentity.GetCurrent();
                    AzRoles.IAzClientContext ctx = 
                       _azManApp.InitializeClientContextFromToken(
                       System.Convert.ToUInt64(id.Token.ToInt64()), 
                       null);
    
                    try
                    {
                        object[] results = (object[])ctx.AccessCheck(
                        controlName, new object[] { "" }, new object[] {
                            System.Convert.ToInt32(opId) }, null, 
                                null, null, null, null);
                            // no error means access granted
                            result = ((int)results[0] == 0);   
                    }
                    catch (Exception ex)
                    {
                        // not fatal
              // TODO: Log error
                    }
                }
            }
        }
        catch (Exception ex)
        {
            // TODO: Log error
            // Don't keep doing same...
            _azManCheck = false;
        }
    
        return result;
    }
    

    Visual Basic:

    Imports System.Reflection
    
    Private Sub CheckSecurity(ByVal o As Object, _ 
    ByVal processedControls As List)
            ' Avoid stack overflow due to properties like Parent 
            ' or TopLevelControl         
            If (o = Nothing) Then
                Return
            End If
            Dim hash As Integer = o.GetHashCode
            Dim processed As Boolean = false
            For Each ctrlHash As Integer In processedControls
                If (ctrlHash = hash) Then
                    processed = true
                                    Exit For
                End If
            Next
            If (processed = true) Then
                Return
            End If
            processedControls.Add(hash)
            Dim fields() As System.Reflection.FieldInfo = _
                o.GetType.GetFields((BindingFlags.Instance Or _
                BindingFlags.NonPublic))
            For Each field As System.Reflection.FieldInfo In fields
                Dim attrs() As Object = field.GetCustomAttributes( _
                    GetType(YourCompany.AuthorizationAttribute), false)
                If (attrs.Length > 0) Then
                    ' We only allow one attribute (AllowMultiple = false)
                    Dim attr As YourCompany.AuthorizationAttribute = _
                        CType(attrs(0),YourCompany.AuthorizationAttribute)
                    Try 
                        If (HasAccess(field.Name, attr.OperationName) = _
                            false) Then
                            Dim ctrl As Object = field.GetValue(o)
                            Dim propInfo As PropertyInfo = _
                                ctrl.GetType.GetProperty(attr.PropertyName)
                            If (Not (propInfo) Is Nothing) Then
                                propInfo.SetValue(ctrl, _
                                    attr.PropertyValue, Nothing)
                            End If
                        End If
                    Catch ex As Exception
                        ' not fatal
                        ' TODO: log the error 
                    End Try
                End If
                If field.GetType.IsClass Then
                    Dim childO As Object = field.GetValue(o)
                    If (childO = System.ComponentModel.Component) Then
                        CheckSecurity(childO, processedControls)
                    ElseIf (childO = IEnumerable) Then
                        For Each item As Object In _ CType(childO,IEnumerable)
                            If (item = System.ComponentModel.Component) _ Then
                                CheckSecurity(item, processedControls)
                            End If
                        Next
                    End If
                End If
            Next
        End Sub
    
        Private Function HasAccess(ByVal controlName As String, _
            ByVal operationName As String) As Boolean
            Dim result As Boolean = false
            Try 
                If (_azManCheck = true) Then
                    If (_azManApp = Nothing) Then
                        ' NOTE: alternatively, you could use
                        '     System.Configuration.ConfigurationManager
                        ' IMPORTANT: This is a demo-quality 
                        ' implementation.  For real production usage, 
                        ' add caching, etc.!!!
                        _configDoc.Load(String.Format( _
                            "{0}{1}.exe.config", _
                            System.AppDomain.CurrentDomain.BaseDirectory, _
                 System.Diagnostics.Process.GetCurrentProcess.ProcessName))
                        Dim authorizationStore As String = _
        _configDoc.DocumentElement.SelectSingleNode( _
            "applicationSettings/YourCompany.AuthorizationSettings" + _
            "/setting[@name='AuthorizationStore']").InnerText
                        If ((authorizationStore = Nothing)  _
                                    OrElse (authorizationStore.Length = 0)) Then
                            If (_azManApp = Nothing) Then
                                ' TODO: Log error
                                ' Don't keep doing same...
                                _azManCheck = false
                            Else
                                Dim store As AzRoles.AzAuthorizationStore _
                                    = New AzRoles.AzAuthorizationStoreClass
                                store.Initialize(0, _
                                    String.Format("msxml://{0}{1}", _
                       System.AppDomain.CurrentDomain.BaseDirectory, _
                                    authorizationStore), _
                                    Nothing)
                                Dim appName As String = _
                        _configDoc.DocumentElement.SelectSingleNode( _
                "applicationSettings/YourCompany.AuthorizationSettings" + _
                "/setting[@name='AzManApplicationName']").InnerText
                                _azManApp = store.OpenApplication( _
                                    appName, Nothing)
                                If (_azManApp = Nothing) Then
                                    ' TODO: Log error
                                    ' Don't keep doing same...
                                    _azManCheck = false
                                End If
                            End If
                        End If
                    End If
                    If (Not (_azManApp) Is Nothing) Then
                        ' Map operation name to id
                        Dim opId As String = _
                            configDoc.DocumentElement.SelectSingleNode( _
                                String.Format("applicationSettings/" + _
               "YourCompany.AuthorizationSettings/setting[@name='{0}']", _
                                operationName)).InnerText
                        If ((opId = Nothing)  _
                                    OrElse (opId.ToString.Length = 0)) Then
                            ' TODO: Log error   
                        Else
                            Dim id As _
                                System.Security.Principal.WindowsIdentity _
            = CType(System.Security.Principal.WindowsIdentity.GetCurrent, _
                                System.Security.Principal.WindowsIdentity)
                            Dim ctx As AzRoles.IAzClientContext = _
                              _azManApp.InitializeClientContextFromToken( _
                        System.Convert.ToUInt64(id.Token.ToInt64), Nothing)
                            Try 
                                Dim results() As Object = _
                                    CType(ctx.AccessCheck(controlName, _
                                    New Object() {""}, _
                             New Object() {System.Convert.ToInt32(opId)}, _
                                    Nothing, Nothing, Nothing, _
                                    Nothing, Nothing),Object())
                                ' no error means access granted
                                result = (CType(results(0),Integer) = 0)
                            Catch ex As Exception
                                ' not fatal
                                ' TODO: Log error
                            End Try
                        End If
                    End If
                End If
            Catch ex As Exception
                ' TODO: Log error
                ' Don't keep doing same...
                _azManCheck = false
            End Try
    
            Return result
        End Function
    

Step 5: Map AzMan Operation IDs to AuthorizationAttribute Operation Names

The currently available version of AzMan doesn't have a way to look up operations by name, just by IDs. This step may eventually be eliminated… Until then, add the mapping of your AzMan operation IDs and names to the application configuration file in the ApplicationSettings section:

<YourCompany.AuthorizationSettings>
<setting name="AuthorizationStore" serializeAs="String">
      <value>YourAzManConfigFile.xml</value>
   </setting>
   <setting name="AzManApplicationName" serializeAs="String">
      <value>YourApplicationName</value>
   </setting>
   <setting name="EditSalary" serializeAs="String">
      <value>1</value>
   </setting>         
</YourCompany.AuthorizationSettings>

That's it, folks. Now, run your application and, based on the AzMan settings and on the AuthorizationAttribute parameters, the text boxes will become read-only, buttons hidden or disabled, and so on. Just like the doctor ordered, and without a single IsInRole call on your forms.

Conclusion

Attribute Based Authorization is a clean and easy implementation that provides a great way to secure your application with almost no coding through design time attributes. While the code in this article is not indented to be used "as-is," it provides a starting point for implementing attribute based authorization into your enterprise.

Other Suggested Reading

Introduction to Role-Based Security

Role-Based Access Controls by David Ferraiolo and Richard Kuhn

Advantages of Role-based Authorization

Use Role-Based Security in Your Middle Tier .NET Apps with Authorization Manager

AzMan Download