Contents
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

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: Imports System.Runtime.Remoting.Contexts
Imports System.Runtime.Remoting.Activation
Imports System.Runtime.Remoting.Messaging
Imports System.Reflection
Namespace AccessControl
' This attribute is applied to our Measurement class;
' it injects the AccessControlProperty to the context,
' which, in turn, adds the AccessControlServerSink to
' the context.
<ATTRIBUTEUSAGE(ATTRIBUTETARGETS.CLASS)> _
Public Class AccessControlAttribute
Inherits ContextAttribute
Public Sub New()
' Call the base class' constructor providing it with
' the attribute name.
MyBase.New("AccessControlAttribute")
End Sub
Public Overrides Sub GetPropertiesForNewContext( _
ByVal ctorMsg As IConstructionCallMessage)
' Add our access control property to the new context.
ctorMsg.ContextProperties.Add(New AccessControlProperty)
End Sub
Public Overrides Function IsContextOK( _
ByVal ctx As Context, _
ByVal ctorMsg As IConstructionCallMessage) As Boolean
' Does the context already have the property and,
' is it of valid type?
Return AccessControlProperty.ExistsInContext(ctx)
End Function
End Class
' The property injects the AccessControlSink into the context
' with which it is associated.
Public Class AccessControlProperty
Implements IContextProperty
Implements IContributeServerContextSink
Public Const PropertyName As String = "AccessControlProperty"
Public Sub Freeze( _
ByVal newContext As Context) _
Implements IContextProperty.Freeze
End Sub
Public Function IsNewContextOK( _
ByVal newCtx As Context) As Boolean _
Implements IContextProperty.IsNewContextOK
' Verify the context has this property.
Return ExistsInContext(newCtx)
End Function
Public ReadOnly Property Name() As String _
Implements IContextProperty.Name
Get
Return PropertyName
End Get
End Property
' Shared helper employed by this class as well
' as the AccessControlAttribute class.
Public Shared Function ExistsInContext( _
ByVal ctx As Context) As Boolean
Dim ExistingProperty As IContextProperty = _
ctx.GetProperty(PropertyName)
If (ExistingProperty Is Nothing) OrElse _
(Not TypeOf ExistingProperty Is AccessControlProperty) Then
Return False
End If
Return True
End Function
' Called by CLR when setting up a new context.
' Injects our sink into the context's sink chain.
Public Function GetServerContextSink( _
ByVal nextSink As IMessageSink) As IMessageSink _
Implements IContributeServerContextSink.GetServerContextSink
Return New AccessControlServerSink(nextSink)
End Function
End Class
' Preprocesses calls to the Measurement class (currently
' the only class within the context containing our
' AccessControlAttribute context attribute).
Public Class AccessControlServerSink
Implements IMessageSink
Private _NextSink As IMessageSink
Public Sub New(ByVal nextSink As IMessageSink)
_NextSink = nextSink
End Sub
Public ReadOnly Property NextSink() As IMessageSink _
Implements IMessageSink.NextSink
Get
Return _NextSink
End Get
End Property
Public Function AsyncProcessMessage( _
ByVal msg As IMessage, _
ByVal replySink As IMessageSink) As IMessageCtrl _
Implements IMessageSink.AsyncProcessMessage
' Access control not supported (yet:-)
Return _NextSink.AsyncProcessMessage(msg, replySink)
End Function
Public Function SyncProcessMessage( _
ByVal msg As IMessage) As IMessage _
Implements IMessageSink.SyncProcessMessage
' If AccessCheck returns Nothing, the check has passed
' and we allow the method to execute.
' If it returns a valid return message, it has the
' Exception member set to an access denied exception;
' we return that message and don't execute the method.
Dim RetMsg As IMethodReturnMessage = Me.AccessCheck(msg)
If RetMsg Is Nothing Then
Return _NextSink.SyncProcessMessage(msg)
Else
Return RetMsg
End If
End Function
' Checks to see if execution of the method identified
' by the message is allowed. If the execution is allowed,
' the method return Nothing. If the execution is denied,
' the method returns an IMethodReturnMessage with the
' Exception member initialized.
Private Function AccessCheck( _
ByVal msg As IMessage) As IMethodReturnMessage
If Not TypeOf msg Is IMethodCallMessage Then
Return Nothing
End If
Dim CallMsg As IMethodCallMessage = _
DirectCast(msg, IMethodCallMessage)
If AccessControlGuard.CanExecuteMethod(CallMsg.MethodName) Then
Return Nothing
End If
Return New ReturnMessage( _
AccessControlGuard.GetAccessDeniedException(), _
CallMsg)
End Function
End Class
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
Public Shared Function GetAccessDeniedException() As _
System.Security.SecurityException
Return New System.Security.SecurityException( _
"Nemáte oprávnenie vykona požadovaný príkaz.")
End Function
End Class
End Namespace
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
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.
Top 