Quick and easy user-level security checks
Posted on 2010-04-29 17:02 Happy Coding 阅读(286) 评论(0) 编辑 收藏 举报Introduction
This article presents a library that enables a programmer to store and retrieve security credentials from a back-end database. The classes in the library also extend the .NET role-based authorization mechanism by introducing the concept of a "right". A right is a low level security element that can be assigned to a role or an individual user. The library includes a specialized principal class that allows these rights to be verified in much the same way that roles are today through the standard .NET security classes.
The library will work with any compatible .NET language, and may be used in thin-client (ASP.NET) or thick-client environments. The library also exports a simple interface for managing users, roles, and rights. The database code is logically isolated from the rest of the library, so adapting things to work with other databases is very straightforward.
Using the code
The classes that are visible outside the library include:
SecurityManager
- allows for authenticating a user based on a user-name and password.UserManager
- allows for creating and removing users from the database.RoleManager
- allows for creating and removing roles from the database.RightManager
- allows for creating and removing rights from the database.UserRightManager
- allows for creating and removing associations between users and rights.UserRoleManager
- allows for creating and removing associations between users and roles.RoleRightManager
- allows for creating and removing associations between roles and rights.CustomIdentity
- implementsIIdentity
and provides access to some additional user specific properties.CustomPrincipal
- implementsIPrincipal
and provides the ability to perform rights authorization.SecurityException
- allows the library to signal errors that are specific to the library.SecurityDataException
- allows the library to signal errors that are specific to the database access code.
The database is pretty simple, and includes the following tables:
As you have probably figured out, I created the default database using Microsoft Access. Remember, you can use any other kind of database by simply writing a bit of database code and modifying the application configuration file. In fact, I intend to write an article in the future that will describe just how to go about doing that.
For now, I will describe how to use the library by looking at how it interacts with the demonstration application. The demo is certainly not going to win any UI design awards but it will allow you to play with the library.
The demo starts by prompting you for a user-name and password, like this:
This is not just for show, the credentials you provide determine what you will be allowed to do once you get into the demo. Start by using 'admin' for a user-name and leave the password blank. The main screen of the demo includes a list of users. Selecting a user from the list populates the tabs on the right. Those tabs present the user properties, along with any roles and/or rights that have been associated with the user. You may use this demo to poke around and get a feel for the capabilities of the library.
If you like, you can log into the demo as something other than the administrator. For instance, logging in as 'user' (again no password), will restrict some of the things you are able to do in the application. After all, it wouldn't do at all to have unauthorized users changing your security settings - right?
There is a menu choice called 'Smart GUI' that looks like this:
Checking this menu creates some basic "intelligence" in the GUI that disables certain buttons whenever a user logs in that isn't a member of the 'Admin' role. I added this feature because the library will throw security exceptions if a non 'Admin' user attempts to perform certain actions. If you want to see these exceptions then turn the "Smart GUI" feature off. Just remember, the security exceptions are not bugs, they are deliberately thrown in order to prevent unauthorized users from making changes to the database.
Here is a quick list of the library features that require the current user to be in the 'Admin' role:
- Creating, deleting or updating a user - except for changing the password.
- Creating, deleting or updating a role.
- Creating, deleting or updating a right.
- Changing the role associations for a user.
- Changing the right associations for a user.
- Changing the right associations for a role.
The library requires the current user to be authenticated in order to change a user's password. In addition, if you are trying to change the password for a user other than yourself, you must be a member of the 'Admin' role. Everything else in the library may be performed by any user.
Let's see how the demo interacts with the library. If you press the 'Add User' button, the following code is executed:
private void _DoAddUser(){ AddUserForm form = new AddUserForm(); if (form.ShowDialog(this) != DialogResult.OK) return; UserManager.Create( form.UserName, form.Password ); } // End _DoAddUser()
The UserManager.Create
method creates a new row in the cg_security_user table, and returns the new identifier. That is all you need to do in order to create a new user. Removing a user is just as easy; pressing the 'Del User' button causes this code to be executed:
private void _DoDelUser(){ // Get the current user index. int userIndex = m_listBoxUsers.SelectedIndex; // Get the identifiers. int userID = (Int32)m_userTable.Rows[userIndex]["user_id"]; UserManager.Delete(userID);} // End _DoDelUser()
The UserManager.Delete
method simply removes the row from cg_security_user, along with any associated rows in other tables. The other manager classes perform similar functions in their own respective areas. It really is pretty easy to use the library, so I won't bother detailing every method of every class.
Points of Interest
I think the most interesting classes in the library are CustomIdentity
and CustomPrincipal
. These classes should be the primary interface between your code and this library. Since these classes are derived from IPrincipal
and IIdentity
respectively, you may use them in the same way you would a standard .NET security object. For those times when you want to use some other authentication scheme, you can still use this library, just use your current identity class to create a CustomPrincipal
object. Provided the user is in the database, everything should still work correctly. I sometimes use this approach to give Windows users additional rights for my own devious purposes.
Just in case you are wondering, here is an example (again from the demo) of how to use the CustomPrincipal
and CustomIdentity
classes in your application:
private void MainForm_Load(object sender, System.EventArgs e){ try { // Prompt the user for credentials. LoginForm form = new LoginForm(); // Did the user fail to provide credentials? if (form.ShowDialog(this) != DialogResult.OK) { this.Close(); return; } // End if the user failed to login. // Did we fail to authenticate the credentials? if (!SecurityManager.Authenticate(form.UserName, form.Password)) { MessageBox.Show("Failed to authenticate the user!"); this.Close(); return; } // End if the credentials were not authenticated. // Set the default principal for the rest of the application. System.AppDomain.CurrentDomain.SetThreadPrincipal( new CustomPrincipal(new CustomIdentity(form.UserName)) ); } // End try catch (Exception ex) { MessageBox.Show(ex.Message); this.Close(); } // End catch} // End MainForm_Load()
Once the principal is set, .NET uses it just as it would any other principal. When you perform security checks in your code, simply check for the CustomPrincipal
like this (from the UserManager.UpdatePassword
method):
// Check that the user is authenticated before we proceed.if (!Thread.CurrentPrincipal.Identity.IsAuthenticated) throw new SecurityException("User must be authenticated" + " in order to perform this action!");// If the calling application is using our identity class, then // we can perform some additional verification.if (Thread.CurrentPrincipal.Identity.AuthenticationType == "Custom"){ // Get the identity class. CustomIdentity identity = (CustomIdentity)Thread.CurrentPrincipal.Identity; // If the user isn't attempting to change their own password then we // should verify that they are acting as an administrator before we // proceed. if (identity.UserID != userID) { // Check the role of the user before we proceed. if (!Thread.CurrentPrincipal.IsInRole("Admin")) throw new SecurityException("User must be in the Admin" + " role to perform this action!"); } // End if the user should be an administrator.} // End if we should verify the identity/role of the user.
Using the library to check for a user's rights could be done like this:
private bool _CheckRight(string rightName){ if (Thread.CurrentPrincipal.Identity.AuthenticationType != "Custom") return false; CustomPrincipal cp = (CustomProncipal)Thread.CurrentPrincipal; return cp.IsAuthorized(rightName);} // End _CheckRight()
I demand to know my rights!
So, how do you know what rights a user will end up with? Simple, begin by selecting a user, then answer these three questions:
- What roles (if any) have been associated with the user?
- What rights (if any) have been associated with those roles?
- What rights (if any) have been specifically associated with the user?
The first and second questions are fairly straightforward, if you assign one or more roles to a user and those roles each have one or more rights assigned to them, then the rights assigned to the user will be the combination of the two lists. If you don't assign any roles to a user then they won't have any rights. If you don't assign any right to the roles then again the user wont have any rights.
The third question is a little trickier because rights that are associated directly with a user override any other setting. Normally, you should probably stick to assigning rights through role associations, but every now and then, you may come across users that defy your ability to categorize them into a predefined role. In that case, you can turn a right on or off for a user by creating an association at this level and then enabling or disabling the association. Enabling it means the user will always have the associated right, disabling it means the user will never have the associated right.
Here is an example:
- Let's say a user is a member of role 'A' and role 'B'.
- Let's also say that role 'A' has right '1', and role 'B' has right '2'.
- At this point, the rights for the user would be: '1' and '2'.
- If we then add a disabled association between the user and right '1', the rights for the user become '2'.
- If we go on to add an enabled association between the user and right '3', then the rights for the user become: '2' and '3'.
Conclusion
I didn't get involved in the internals of the library much since it is mostly standard database code with some plumbing thrown in to provide logical isolation from the rest of the library. I will talk about library internals more when I discuss how to implement a data access library for SQL-Server.
Until then, play around with the demo application and consider how you might use the library in your next project.
Have fun! :o)
History
- Library version 2.4 - Original release on CodeProject.
- Library version 2.5 - Various small fixes for things that escaped me when I created the original article.