Vibrant discussion about CSLA .NET and using the framework to build great business applications.
I am developing a class to be used as the Central Point of access to Control All Functions for All Application.
In general, the class will provide the following functionality:
For a given Application, Who Can Do What and When.
I need your input and feedback to implement this class in the best way possible, and if you have other suggestions, please feel free to let me know.
Sample Use for this Class:
For example, we have Attendance System which is live for the past 4 years. Everyone will punch the in/out times. Some times, staff will forget to punch in or out ! So, I created a Screen to Enter Attendance Adjustments.
Of course, this screen need to be secured. Not any one can enter adjustment. Right ?
So, the HR Users requested me to provide the screen for 2 main User Roles:
- Application Admin: He can enter the adjustment for any Staff.
- Department Admin: He can enter the adjustment only for the staff who are in the same department of the user who is making the Data Entry.
Now, I have another application which is used to Display Staff Profile Info On-Line (eHRMD), such as Contact Info, Personal Info, Salary, Medical Lab Requests ...etc.
So, the HR Users requested me to provide extreme flexibility to allow Authorized Staff to view the Profile Data of other Staff based on predefined rules.
For example:
- Application Admin: Can view staff info of any other staff.
- Director: Can view all Staff Info EXCEPT Medical Data.
- Section Head: Can view all Staff Info EXCEPT Medical Data and Salary Data
...etc...
Database Design:
I decided to make the Database as follows:
Following is a Sample of each table above:
Application Security Class Implementation:
I decided to implement the class as follows:
- Class Name: AppSecurity
- Use Singleton Pattern: "appsec = AppSecurity.GetSingleton()" to get an instance of the class.
- Load the Tables above into memory using DataSet only once during the Application Life in the Worker Process. This is to avoid frequent Database access.
- Use CSLA .NET DataProtal Fetch method to load the Data. This is to make use of Mobile Business Object, when needed.
- Use DataRelation to relate the tables in the DataSet.
- Use CSLA .NET Bindable Objects only when need to bind the Data with the Presentation Layer.
- To check for security, following is the Application UI sample Code of the Attendance Adjustment Entry Screen (note that this is for clarification purposes, as such code must be encapsulated in the Business Logic Layer in a special class):
sub Page_Load(...) appsec = AppSecurity.GetSingleton() if (appsec.CanPerformAction(AppCodes.AtSys, UIElmCodes.UI001, UIElmActCodes.AC001) then ' Means can Enter Adjustment for All Staff. ' Yes, he is authorized, continue .. ' Setup the DataSource of the Screen to work with all Staff elseif (appsec.CanPeformAction(AppCodes.AtSys, UIElmCodes.UI001, UIElmActCodes.AC003) then ' Means can Enter Adjustment for only for Staff in the same Dept. ' Get the Dept. Code of the Loged In User. ' Filter the DataSource of the Screen to allow only working with ' Staff who are in the same Dept. of the Loged in user. else response.write("Access Denied or something like that." end ifend sub
Why not using .NET Native Authorization using web.config:
Enabling authorization using web.config and Allow/Deny rules is good to Allow or Deny Specific User/Roles to "Open" certain pages/links within the web site (with an option for Security Trimming", but using this technique "as-is" is not good enough. Why ? Because the logic also depends on further data to be retrieved from the Database.
Using web.config without further work also has additional problem: You have to hard-code the authorized Roles within the web.config, and hard-code the authorization rules also. In my approach, I am not hard-coding the Rules, instead, I am using predefined Keys to hard-code the meaning of the codes in the program, and allow the user to change the Mapping between the Roles and the Action Codes.
In order to defined the mapping between the Application Functions and User Roles outside the Program, you MUST do additional work. The .NET does not do this work for you. Please correct me if I am wrong. However, .NET gives something called "Authorization Providers", where you can implement them in your program. This is exactly what I am trying to do. In the end, the Implementation of such Provider will have to call the functionality of the "AppSecurity" class which I mentioned earlier.
Sample Code of the Class:
I am posting below the sample code of the class to clarify the concept:
<Serializable()> _Public Class AppSecurity#Region " Hard-Coded Constant Values " ' ' Define the Hard-Coded Values to be used in the program ' Such values are related to the Key Values in the Security Database Tables ' They are defined as Constants in Classes jsut to make them easier to work with ' Public Enum AppCodes eHRMD AtSys End enum Public Enum UIElmCodes UIAll UI001 End Enum Public Enum ULElmActCodes ACAll AC001 AC003 End Enum#End Region Private Shared mSingleton As AppSecurity Private Shared lockobject As New Object Private mAppSecDataSet As DataSet#Region " Factory Methods " Private Sub New() End Sub Public Shared Function GetSingleton() As AppSecurity If mSingleton Is Nothing Then SyncLock lockobject If mSingleton Is Nothing Then mSingleton = DataPortal.Fetch(Of AppSecurity)(New Criteria()) End If End SyncLock End If Return mSingleton End Function#End Region#Region " Data Access " <Serializable()> _ Private Class Criteria Public Sub New() End Sub End Class Private Overloads Sub DataPortal_Fetch(ByVal criteria As Criteria) 'Dim TestEnum As AppCodes 'TestEnum = AppCodes.AtSys 'Console.WriteLine(TestEnum.ToString) Try Using con As New OleDb.OleDbConnection(Database.AppSecurityDB) Using cmd As OleDb.OleDbCommand = con.CreateCommand cmd.CommandType = CommandType.Text Using da As New OleDb.OleDbDataAdapter(cmd) ' ' Load all tables with small set of records into ' memory using DataSets ' cmd.CommandText = "select * from tApplications" mAppSecDataSet = New DataSet("ApplicationSecurity") da.Fill(mAppSecDataSet, "tApplications") cmd.CommandText = "select * from tAppDataTypes" da.Fill(mAppSecDataSet, "tAppDataTypes") cmd.CommandText = "select * from tUIElements" da.Fill(mAppSecDataSet, "tUIElements") cmd.CommandText = "select * from tUIElmActions" da.Fill(mAppSecDataSet, "tUIElmActions") cmd.CommandText = "select * from tRoles" da.Fill(mAppSecDataSet, "tRoles") cmd.CommandText = "select * from tAuthorization" da.Fill(mAppSecDataSet, "tAuthorizations") ' ' Any lookup required against the Staff or Staff Roles ' will have to be done against the Database directly. ' This is to avoid loading large number of records ' ' ' Now define the Relationship among the tables in the DataSet ' Dim ParentColumns(0 To 0) As DataColumn Dim ChildColumns(0 To 0) As DataColumn Dim Rel As DataRelation ParentColumns(0) = mAppSecDataSet.Tables("tApplications").Columns("AppID") ChildColumns(0) = mAppSecDataSet.Tables("tAppDataTypes").Columns("AppID") Rel = New DataRelation("App_DataTypes", ParentColumns, ChildColumns) mAppSecDataSet.Relations.Add(Rel) Rel = Nothing ReDim ParentColumns(0 To 0) ReDim ChildColumns(0 To 0) ParentColumns(0) = mAppSecDataSet.Tables("tApplications").Columns("AppID") ChildColumns(0) = mAppSecDataSet.Tables("tUIElements").Columns("AppID") Rel = New DataRelation("App_UIElements", ParentColumns, ChildColumns) mAppSecDataSet.Relations.Add(Rel) Rel = Nothing ReDim ParentColumns(0 To 1) ReDim ChildColumns(0 To 1) ParentColumns(0) = mAppSecDataSet.Tables("tUIElements").Columns("AppID") ParentColumns(1) = mAppSecDataSet.Tables("tUIElements").Columns("UIElemID") ChildColumns(0) = mAppSecDataSet.Tables("tUIElmActions").Columns("AppID") ChildColumns(1) = mAppSecDataSet.Tables("tUIElmActions").Columns("UIElemID") Rel = New DataRelation("UIElements_Actions", ParentColumns, ChildColumns) mAppSecDataSet.Relations.Add(Rel) Rel = Nothing ReDim ParentColumns(0 To 2) ReDim ChildColumns(0 To 2) ParentColumns(0) = mAppSecDataSet.Tables("tUIElmActions").Columns("AppID") ParentColumns(1) = mAppSecDataSet.Tables("tUIElmActions").Columns("UIElemID") ParentColumns(2) = mAppSecDataSet.Tables("tUIElmActions").Columns("ActionID") ChildColumns(0) = mAppSecDataSet.Tables("tAuthorization").Columns("AppID") ChildColumns(1) = mAppSecDataSet.Tables("tAuthorization").Columns("UIElmID") ChildColumns(2) = mAppSecDataSet.Tables("tAuthorization").Columns("ActionID") Rel = New DataRelation("Actions_Authorization", ParentColumns, ChildColumns) mAppSecDataSet.Relations.Add(Rel) Rel = Nothing ReDim ParentColumns(0 To 0) ReDim ChildColumns(0 To 0) ParentColumns(0) = mAppSecDataSet.Tables("tRoles").Columns("RoleID") ChildColumns(0) = mAppSecDataSet.Tables("tAuthorization").Columns("RoleID") Rel = New DataRelation("Roles_Authorizations", ParentColumns, ChildColumns) mAppSecDataSet.Relations.Add(Rel) Rel = Nothing End Using End Using End Using Catch ex As Exception Throw New Exception(ex.Message, ex) End Try End Sub#End RegionEnd Class
Questions:
- What is the best approach to use CSLA .NET Framework to Implment this Logic ? I am finding some difficulty to fit this in CSLA .NET Authorizations.
If you think there is something wrong or you have any suggestions, I appreciate your feedback.
Tarek.
Dear All,
I really need your help !
My question here is based on the post above.
I have read Chapter 1,2, 6, 7, 8 and 10 of the Expert VB 2005 Business Objects.
I am having some difficulty in fitting my requirement (as per above) into CSLA .NET Model.
The problem I am facing is in Authorization.
Let us take a simple Example:
1. User A logs on to the Staff Profile Application (Windows Authentication), and he can view his HR related data.
2. User A requests to view the Staff Profile of another User B, and he requested to view certain data (Education History for example).
3. The logic to allow User A to view the Education History of User B, if all the following validation check are TRUE:
- If User A is in the Manager Role,
- If User A is in the same Department of User B.
- If the sensitivity level of the Education History is less than the max sensitivity level allowed form the Manager Role.
As you can see is that in order to perform the Authorization Check, the code must perform several lookups to the Security Control Data and to the Profile Data. This means the authorization depends on:
- The Department Code of the Authenticated User and the Dept. Code of the requested user,
- The Role of the Authenticated User,
- The sensitivity level of the request user info and the User Role of the authenticated user.
Now going back to CSLA .NET, in the Authorization Section, the relevant Authorization method that applies here is this one:
Public Shared Function CanGetObject() As Boolean Return Csla.ApplicationContext.User.IsInRole("Some Roles")End Function
However, as you can see, the Role is hard-coded in the method, and there is no indication on how to perform lookup within this method using CSLA Approach via the Data Portal FETCH method. Because as per the book, the FETCH will occur after the check for CanGetObject().
The same problem I will face while checking for Authorization for each property:
AuthorizationRules.AllowRead("PropertyName", "SomeRole")andCanReadProperty()
Also, the same lookup mentioned above is needed to check for authorization, which really does not make sense to perform such lookup for each property and for the CanGetObject() method !!!!!##### This will make the application response very slow, because the number of properties if very large (more than 20 properties).
The other problem I am facing is that the control info changes from one case to another (depends on the selected user), this means the method checking for authorization cannot depend on Shared Properties.
Check the standard code for GetStaffProfile():
Public Shared Function GetStaffProfile(ByVal id As String) As StaffProfile If Not CanGetObject() Then Throw New System.Security.SecurityException( _ "User not authorized to view staff profile of this Staff.") End If Return DataPortal.Fetch(Of StaffProfile)(New Criteria(id))End Function
Because I was thinking to load the control info once at the beginning, then perform the check when needed, but seems this is not possible because this is a shared method "GetStaffProfile()".
This means that I must first create the object, load the control info, then perform authorization check. So, I was thinking to change the methods above as follows:
<Serializable()> _public class StaffProfileControl' Add ControlCriteria to be used to call the FETCH of the Control Info<Serializable()> _Private Class ControlCriteria Private mId As Guid Public ReadOnly Property Id() As Guid Get Return mId End Get End Property Public Sub New(ByVal id As Guid) mId = id End SubEnd Class'Add the call to FETCH the Control Info the Staff Profile as followsPublic Shared Function GetStaffProfileControl(ByVal id As Guid) As StaffProfileControl Return DataPortal.Fetch(Of StaffProfileControl)(New ControlCriteria(id))End Function...... the rest almost the same...End Class
The idea here is to FIRST OF ALL, do fetch the Control Info, and create an new special control object of StaffProfile which has only the control info. So, in the UI Code, this is the first call to be made and any user can access the control info of this object, so no need to do any authorization here.
Then, during the subsequent calls to load the actual StaffProfile object, we can pass the StaffProfileControl Object as a parameter to the methods where needed.
I hope this is the correct way for implementing my requirements using CSLA .NET.
Your feedback will be appreciated.
tarekahf: Now going back to CSLA .NET, in the Authorization Section, the relevant Authorization method that applies here is this one:Public Shared Function CanGetObject() As Boolean Return Csla.ApplicationContext.User.IsInRole("Some Roles")End Function However, as you can see, the Role is hard-coded in the method, and there is no indication on how to perform lookup within this method using CSLA Approach via the Data Portal FETCH method. Because as per the book, the FETCH will occur after the check for CanGetObject().
This is not part of CSLA, this code is in your business class. It is just an example of what you might do, not of what you must do. You can replace this with any other code you'd like. And you can move it to another location.
You might choose to do this check inside DataPortal_Fetch() because that's the only place you can really do the check. That's fine, CSLA doesn't care.
tarekahf: The same problem I will face while checking for Authorization for each property:AuthorizationRules.AllowRead("PropertyName", "SomeRole")andCanReadProperty()
You can override CanReadProperty() and CanWriteProperty() to customize their behavior for a specific type of object. Obviously the implementation in BusinessBase just checks roles, but you could choose to do other checks before or after letting the base class do its work.
Rocky
RockfordLhotka: This is not part of CSLA, this code is in your business class. It is just an example of what you might do, not of what you must do. You can replace this with any other code you'd like. And you can move it to another location. You might choose to do this check inside DataPortal_Fetch() because that's the only place you can really do the check. That's fine, CSLA doesn't care. You can override CanReadProperty() and CanWriteProperty() to customize their behavior for a specific type of object. Obviously the implementation in BusinessBase just checks roles, but you could choose to do other checks before or after letting the base class do its work.
Thank you Rocky,
OK, I think I understand what you mean. However, please allow me to summarize the problem, as the previous post I made was very long and complicated.
The problem:
1. In CSLA, checking for authorization is done in Shared Methods, which means you cannot first load the control info into private properties which could be used later for checking if certain operation is authorized for some user.
2. Checking for authorization has nearly same logic in the authorization methods [ Ex. CanReadProperty() and CanGetObject() ], and will have to be repeated. So, if the check requires first to perform Database Access (look-up), this means such data must be made available in private properties so that it can be reused when needed to avoid making Database Lookups over and over.
Please correct me if I misunderstood you.
You suggested to move the check to inside DataPortal_Fetch(), so at this time the base object is created, and I can perform the needed lookup to load the control data into private properties. Is my understanding correct ?
But, I will have the same requirement for authorization checking for the other DataPortal_XXX methods, which may require same control data, or probably others.
That is why I was thinking to create a CSLA .NET Base Object to represent the Control Data. This object will have to be created in the UI code before working with the other regular business objects. And also, it can be cashed so that it can be used by other pages ...etc.
Suppose I have a BO called "StaffProfile". So, I will create another object called "StaffProfileControl" for example. It will be based on CSLA Read Only Business Base, and it is just to load the Control Data into its public properties. And, I will pass this control object as a parameter to the Factory Methods of the regular BO, so that it can be stored as a provide property inside this object for later use any time needed by any other method in this object.
So, the UI Code will do something like this:
dim ctrl as StaffProfileControlctrl = StaffProfileControl.getObject(AuthenticatedStaffID, AnotherStaffID)dim theProfile as StaffProfiletheProfile = StaffProfile.getObject(ctrl)......theProfile.save()
So, the "ctrl" object will have the necessary data to perform authorization checking, and it can have also any complex code needed for implement the checking, and can be reused over and over without any performance overhead.
You feedback will be appreciated, because I think this is a bit complicated, and my colleagues may not like this approach. I am now trying to make CSLA .NET as part of our standards for Application Development.
There are three things here I think.
1. You can use Shared/static methods to implement per-type authorization – authorization that is not specific to any instance of an object, but is common for all instances of a given type. That is what you get with the CanGetObject() and similar methods shown in the book. And this is what you get with CSLA 3.5, which formalizes this concept and makes it part of the framework.
2. You can write your own authorization methods to do per-instance authorization. You need to decide when and where to do these checks, but one logical place is at the top (or bottom) of each DataPortal_XYZ method, because that’s the first opportunity you have to check the security principal against the object instance. Another logical place to do per-instance authorization is in your factory methods and an override of the Save() method. These are also locations where you have access to the object instance and the security principal.
3. You can do per-property authorization using CanReadProperty() and CanWriteProperty(). These methods can be overridden so you can customize or replace the default role-based behavior.
If you choose to override the per-property authorization, you must remember that your code will run on the client and/or the server. And it will be called A LOT. So you must make the code efficient. The user’s information should be in the principal/identity objects, and you may want to cache the results (CSLA does) so you don’t need to re-check the per-property authz each time.
If you choose to implement per-instance authorization in the factory methods and a Save() override, you must remember that your code will run on the client. Again, you need to make this code reasonably efficient – though this isn’t as critical as with per-property, because the factory and Save() methods aren’t called nearly as often.
Rocky, Thank you again ... the explanation you provided was really so useful.
Please allow me to clarify some points.
RockfordLhotka: There are three things here I think. 1. You can use Shared/static methods to implement per-type authorization – authorization that is not specific to any instance of an object, but is common for all instances of a given type. That is what you get with the CanGetObject() and similar methods shown in the book. And this is what you get with CSLA 3.5, which formalizes this concept and makes it part of the framework.OK, I will use this type of authorization when the checking does not require Database Access (lookup), and in case it will only depend on Role-Based Authorization. Right ? You can write your own authorization methods to do per-instance authorization. You need to decide when and where to do these checks, but one logical place is at the top (or bottom) of each DataPortal_XYZ method, because that’s the first opportunity you have to check the security principal against the object instance.This means I will have to load the Control Info inside the DataPortal_XYZ method, and do the check before any further processing or data access. So there is no need to create another Object for loading the control data ?.Another logical place to do per-instance authorization is in your factory methods and an override of the Save() method. These are also locations where you have access to the object instance and the security principal.In the factory method ??!! you mean after executing this call:myStaffProfile = DataPortal.Fetch(Of StaffProfile)(New Criteria(id))Because only after this call, I will have a instance of the object "myStaffProfile". But this means that authorization checking is done after completing the load of the entire private properties, which may not be wise to do so. I think it is better to check for authorization before loading any other data inside DataPortal_XYZ call.Another thing which confusing me now is that you are saying that the "security principal" I have access to it in the Factor Methods ...etc. I thought this security principal, I have access to it any time any where in the code during run-time, right ?Just to confirm that for authorization checking, not only I need to access the control info of the authenticated user, but also need to have access to the control info of the Staff ID which is requested by the authenticated user. This control info is the Department Code of and User Role, and probably few other data fields. The user’s information should be in the principal/identity objects, and you may want to cache the results (CSLA does) so you don’t need to re-check the per-property authz each time.Are you telling me that I have to load the control info (eg. Dept. Code) of the authenticated user in the principal/identity objects ? Actually, I tried to do that, but every time I need to get the Dept. Code of the authenticated user using the identity object, then I have to CAST (Convert the Type of) the object to the custom identity object defined in my project. Is my understanding correct ?Your feedback is always appreciated.Tarek.
You can write your own authorization methods to do per-instance authorization. You need to decide when and where to do these checks, but one logical place is at the top (or bottom) of each DataPortal_XYZ method, because that’s the first opportunity you have to check the security principal against the object instance.
Another logical place to do per-instance authorization is in your factory methods and an override of the Save() method. These are also locations where you have access to the object instance and the security principal.
myStaffProfile = DataPortal.Fetch(Of StaffProfile)(New Criteria(id))
Because only after this call, I will have a instance of the object "myStaffProfile". But this means that authorization checking is done after completing the load of the entire private properties, which may not be wise to do so. I think it is better to check for authorization before loading any other data inside DataPortal_XYZ call.
Another thing which confusing me now is that you are saying that the "security principal" I have access to it in the Factor Methods ...etc. I thought this security principal, I have access to it any time any where in the code during run-time, right ?
Just to confirm that for authorization checking, not only I need to access the control info of the authenticated user, but also need to have access to the control info of the Staff ID which is requested by the authenticated user. This control info is the Department Code of and User Role, and probably few other data fields.
The user’s information should be in the principal/identity objects, and you may want to cache the results (CSLA does) so you don’t need to re-check the per-property authz each time.
Your feedback is always appreciated.
Hi Rocky,
Just wanted to say thank you ... I have completed the implementation of this Security Class, and I deployed it live for more than one year now.
I have used VS 2005 and CSLA .NET 2.0 and the logic is working like a charm for more than 5 different application objects, and the total number of users is more than 1500 users, and the total number of concurrent users can easily exceed more than 200.
Thanks God, until now, no problem what so ever.