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)
Introduction
Make It Simple
Let Me Show You How...
Conclusion
Other Suggested Reading
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.
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.
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.
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.
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
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.
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
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.
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.
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