Vibrant discussion about CSLA .NET and using the framework to build great business applications.
Can someone show me the syntax for adding an instance level authorization rule in CSLA 4? Rocky's overview of the Authorization Rules for CSLA 4 mentions this functionality but there is no example of how to go about adding this type of rule.
Where possible (specifically for insert/update/delete operations) the Target property of the context parameter is populated with a reference to the business object instance. So your authorization rule can use the Target property to interact with the object instance.
Obviously in the case of create/fetch there is no instance when the rule runs because the rule is indicating whether it is possible to do the create/fetch before the operation occurs - so Target isn't available in those cases.
So in order to use instance level authorization for read, edit, delete, and add, I need to create a custom "IsInRole" class ... correct? Your blog write-up didn't mention that explicitly so I was thinking I could use the Rules.CommonRules.IsInRole version.
Do I need to override the logic in the BusinessBase so that it performs an instance level authorization before a Save or Delete (e.g., Rules.BusinessRules.HasPermission(AuthorizationActions.Delete, _myCustomer) ) or does CSLA already do this for me (check at per-type level and per-instance level)?
In the spirit of helping others, I'll explain the solution I came up with:
My application contains various master data tables (e.g., colors). The Colors table comes pre-populated with some standard entries. These are flagged in the database as "system entries". The user is free to add to this list of color(s). The authorization requirements are as such:
I created a read-only property named "IsSystemEntry" in my ColorBO.
I created a custom IsInRole class named "SysEntryIsInRole". I added another cached list of roles (_SysPptyRoles) to the class to store the role(s) which can perform operations on system entries. The Sub New contains a signature with:
(Action, SysEntryRoles as List(of String), Roles as List(of String))
Since Property level authorizations are already instance based, I did not have to worry about them in my custom class (hence there is no place holder for "element" in the Sub New).
In the Execute method, I check context.target. If it is Nothing then I do the same look-up processing against the _Roles list that the CommonRules.IsInRole does. However, if the target contains a reference, then an instance level authorization is being requested. I cast the target as my ColorBO then reference the IsSystemEntry property. Only deletion and edit authorization rules are special for system entries so if the authorization action is one of these, I loop over the _SysEntryRoles list just like you normally do for the _Roles list; otherwise I loop over the _Roles list.
Here's the code which might make things clearer:
In my ColorBO:
Protected Shared Sub AddObjectAuthorizationRules() Rules.BusinessRules.AddRule(GetType(ColorBO), New Rules.CommonRules.IsInRole(Rules.AuthorizationActions.GetObject, "User")) Rules.BusinessRules.AddRule(GetType(ColorBO), New Rules.CommonRules.IsInRole(Rules.AuthorizationActions.CreateObject, "User")) Rules.BusinessRules.AddRule(GetType(ColorBO), New SysEntryIsInRole(Rules.AuthorizationActions.EditObject, "System Admin", "User")) Rules.BusinessRules.AddRule(GetType(ColorBO), New SysEntryIsInRole(Rules.AuthorizationActions.DeleteObject, "**NoOne**", "User")) End Sub
For the delete rule, I specify the fictitious role "**NoOne**" to indicate no one can perform this task. I could just as well have passed an empty string. Again only Edit and Delete require special checking so only those two actions reference my SysEntryIsInRole class.
In my SysEntryIsInRole class:
Public Class SysEntryIsInRole Inherits Csla.Rules.AuthorizationRule Private _Roles As List(Of String) Private _SysPptyRoles As List(Of String)
'I actually pass in a single string for the SysPptyRole (System Property Role) because I know I will have at most one entry and it saves me
'from having to pass a list
Public Sub New(ByVal Action As Csla.Rules.AuthorizationActions, ByVal SysPptyRole As String, ByVal ParamArray roles() As String) MyBase.New(Action) _Roles = New List(Of String) For Each item As String In roles _Roles.Add(item) Next _SysPptyRoles = New List(Of String) _SysPptyRoles.Add(SysPptyRole) End Sub
Protected Overrides Sub Execute(ByVal context As Csla.Rules.AuthorizationContext) If context.Target Is Nothing Then 'Entity level authorization check CheckAuthorization(context) Else 'Instance level authorization check Dim o As ColorBO = CType(context.Target, ColorBO) If o.IsSystemEntry AndAlso (Me.Action = Csla.Rules.AuthorizationActions.DeleteObject OrElse Me.Action = Csla.Rules.AuthorizationActions.EditObject) Then context.HasPermission = False 'Default is NO If _SysPptyRoles IsNot Nothing Then For Each role As String In _SysPptyRoles If Csla.ApplicationContext.User.IsInRole(role) Then context.HasPermission = True Exit For End If Next End If Else CheckAuthorization(context) End If End If End Sub Private Sub CheckAuthorization(ByVal context As Csla.Rules.AuthorizationContext) If _Roles.Count > 0 Then For Each role As String In _Roles If Csla.ApplicationContext.User.IsInRole(role) Then context.HasPermission = True Exit For End If Next Else context.HasPermission = True 'Default is to authorize unless explicitly stated End If End Sub
Since I have many master tables that require this same authorization checking, I am going to create in Interface for the IsSystemEntry property so that I can cast any BO calling this rule and obtain the value of "IsSystemEntry".
I need to correct something I stated above. This following statement is false:
Since Property level authorizations are already instance based, I did
not have to worry about them in my custom class (hence there is no place
holder for "element" in the Sub New).
The business rules contained in "AddBusinessRules" apply at the BO level (i.e., per-type). The "AddBusinessRules" sub is only called once so you cannot place instance based property level authorization in it.
One of the property level authorization rules I omitted from my example above states:
I mistakenly had code in "AddBusinessRules" as such:
If IsSystemEntry Then 'protect Name BusinessRules.AddRule(New Rules.CommonRules.IsInRole(Rules.AuthorizationActions.WriteProperty, NameProperty, "**NoOne**")) Else BusinessRules.AddRule(New Rules.CommonRules.IsInRole(Rules.AuthorizationActions.WriteProperty, NameProperty, "User")) End If
Since the system entries are the first fetched from the database, the Name property was write protected for every ColorBO ... even those created by the user.
I now realize that I need to handle the read/write authorization for the Name property in my SysEntryIsInRole class since the authorization is based on the IsSystemEntry flag like the instance level edit and delete authorizations.