Thomas Alva Edison's light bulb VBInfoZine Home
   An ordinary VB developer shares his own successes and failures
   FREE to registered subscribers.  

.NET Interception

Using the .NET interception plumbing to add access control functionality to an existing monolithic application.

The application I'm going to talk about in this article is a rather simple one (at least from an architectural point of view). It's used to analyze, display and amend data representing concentrations of various air pollution agents measured at stations spread throughout the Slovak republic (see screenshot - 219 KB, Slovak language only :-().

The data from the stations are collected and consolidated into one huge SQL Server database, which the application reads and updates directly.

Although the application is monolithic, I was fortunate enough (:-)) to divide the application into layers internally. So there is the user interface layer, the business layer and the data access layer, as usually.

An App class represents my data access layer. It contains methods that connect to the database and execute the application's stored procedures. All methods of the App class are static (Shared).

A Measurement class represents the business layer. The class retrieves data from SQL Server by calling the App class' methods, allows the data to be displayed and manipulated by the user and finally, propagates the data modifications back to the database (by calling the App class, of course).

The application has been used for a couple of months already, when I've got a request to implement a new feature - authorization and access control:

Upon startup, the application should present a logon dialog box and validate the user name and password entered against a new _User table in the database.

Each row in the _User table represents an application user (you guessed that :-)) and contains a varchar column UserEnabled representing encoded access rights for several applications. My application has got the 6th character position in the UserEnabled field. (Please, don't ask me why they designed it that way; I don't know.) The 6th character could have the following values:

6th character Meaning
"0" Read-only access; no modifications
"1" Full access
"2" Restricted acces (details bellow)

Any other value (or missing user record altogether) should cause the application to terminate.

After some thinking, I realized that the access rights represented by the 6th character of the UserEnabled column are directly related to methods exposed by the Measurement class. In other words:

  • Full access means that ALL Measurement methods modifying data might be called.
  • Restricted access means that only SOME of the Measurement methods modifying data might be called.
  • Read-only access means that NO Measurement methods modifying data might be called.
Realizing that, I designed the authorization / access control feature as follows:

A new UserToken class wraps the _User table row and hides the awkward UserEnabled column parsing behind nice, read-only Boolean properties CanChangeDataValues (full access) and CanChangeDataAttributes (restricted access).

The App class exposes a new shared, read-only property User() As UserToken. The property is initialized by a new, shared method App.LogonUser(name, password). The App.LogonUser method does the _User table lookup and creates a new UserToken instance from the _User row found. Here is the relevant code from the App class.

A new UserLogonDialog form enables the user to enter her name and password and then calls the App.LogonUser method. The dialog is displayed at application startup.

The access control algorithm itself was designed with extensibility in mind. Because I'm in love with table-driven algorithms, I have chosen a table-driven approach.

The logic is encapsulated in a new AccessControlGuard class. The class has a private Hashtable instance containing String-typed keys and elements. The keys are the Measurement class' method names. The elements are the UserToken class' property names (they have to return True in order for an access check to be passed):

Public Class AccessControlGuard

Private Shared _MethodToPropertyMap As New Hashtable

Shared Sub New()
  _MethodToPropertyMap.Add("MarkValuesInvalid", "CanModifyDataAttributes")
  _MethodToPropertyMap.Add("UpdateMinValues", "CanModifyDataValues")
  _MethodToPropertyMap.Add("ApplyLineConstants", "CanModifyDataValues")
  _MethodToPropertyMap.Add("MultiplyValues", "CanModifyDataValues")
  _MethodToPropertyMap.Add("UploadMeasuredValues", "CanModifyDataAttributes")
End Sub

Public Shared Function CanExecuteMethod( _
 ByVal method As String) As Boolean
  ' Lookup the UserToken property name that must
  ' return True in order to allow the method to execute.
  Dim PropName As String = CStr(_MethodToPropertyMap(method))
  If PropName Is Nothing Then
    ' The method is not in our table so by definition,
    ' we allow it to be executed (it is not data modifi-
    ' cation method) as long as the user token has ANY
    ' access to the application.
    Return Not App.User.NoAccess
  End If

  ' Invoke the property through reflection.
  Dim PropInfo As PropertyInfo = App.User.GetType().GetProperty(PropName)
  Debug.Assert(Not PropInfo Is Nothing)
  Return CBool(PropInfo.GetValue(App.User, Nothing))
End Function

Public Shared Sub AccessCheck( _
  ByVal method As String)
  If Not CanExecuteMethod(method) Then
    Throw GetAccessDeniedException()
  End If
End Sub
...
End Class
The last very important question remains: How to incorporate the access checks into the existing Measurement class?

The 'classic' approach would be to add the appropriate AccessControlGuard.AccessCheck calls at the start of every data modification method of the Measurement class.

But I didn't do it that way.

Recently, I've read a couple of articles about interception in .NET, and this project seemed to be an ideal medium to try the concepts by hand.

So I went out and implemented the interception code according to recommendations found in the great article "Decouple Components by Injecting Custom Services into Your Object's Interception Chain", by Juval Lowy (see links).

Here are the implementation steps for adding interception to the existing Measurement class:

1. I've implemented the AccessControlServerSink class, which gets called for each method call coming INTO the context, where our Measurement class resides:

Public Class AccessControlServerSink
  Implements IMessageSink
2. I've implemented the AccessControlProperty class, which injects the AccessControlServerSink into the Measurement's context:
Public Class AccessControlProperty
  Implements IContextProperty
  Implements IContributeServerContextSink
3. I've implemented the AccessControlAttribute class, which has to be associated with our Measurement class and which adds the AccessControlProperty to the context:
<AttributeUsage(AttributeTargets.Class)> _
Public Class AccessControlAttribute
  Inherits ContextAttribute
4. I've derived the Measurement class from ContextBoundObject class and I've applied the AccessControlAttribute to it:
<AccessControl.AccessControl()> _
Public Class Measurement
  Inherits ContextBoundObject
Here is the complete code for the classes:

I've built the project, tested with sample data and all kinds of users - everything went well.

I was excited!

I've started to write this article eager to share with you how easy is to implement interception in .NET. I didn't finish the article, however, because I had to go home (my wife called me that we are supposed to visit our friends...:-)).

That was yesterday.

Today I came to work to do some more testing and finish the article eventually.

I've run some tests with real data and I've to say that my excitement was gone.

The application turned to be way too slow!

This is not surprising at all, given the way the Measurement class is used within the application. The interception code simply added too much overhead. For example, when the application displays a line graph, the data and attributes for each point are retrieved by calling five Measurement's methods. For 10 000 points that means 50 000 cross-context calls. Huh!

So in the end, I had to remove the interception-related code and I've added the access check to the prolog of every relevant Measurement class' methods.

This is certainly not to say that interception is slow. I still think it's a great technique provided the associated performance penalty is negligible. Remote marshal-by-reference objects, for example, are good candidates for interception, because they are accessed through proxy anyway.

So be careful when considering using interception in your own project. You have to take into account the associated performance overhead. If you are not sure, a quick prototype might be in order. After all, adding the interception code is easy (once you grasp the details, of course).

Obligatory warning: Most of the classes and interfaces discussed here and in the articles referred bellow are officially undocumented, so use them at your own risk. Nevertheless, the fact that they are described in the MSDN Magazine's article might say something, IMHO.

© Palo Mraz, Sunday, August 31, 2003

Links

http://msdn.microsoft.com/msdnmag/issues/03/03/ContextsinNET/default.aspx - great article about .NET contexts and interception.

http://msdn.microsoft.com/msdnmag/issues/02/03/AOP/default.aspx - describes a custom COM interception framework and relates it to the .NET approach.

http://oko.shmu.sk/ - air pollution in the Slovak Republic.

 ©2003-2007 Palo Mraz. All Rights Reserved.   See my 'new browser window' policy