Click here to Skip to main content
Click here to Skip to main content

Quick and easy user-level security checks

By , 3 Apr 2005
 

Sample Image - CGSecurity1.jpg

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 - implements IIdentity and provides access to some additional user specific properties.
  • CustomPrincipal - implements IPrincipal 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Martin Cook
Web Developer
United States United States
Member
I am a C# developer specializing in creating object-oriented software for Microsoft Windows. When I am not programming I enjoy reading, playing the guitar/piano, running, watching New York Ranger hockey, designing and building wacky electronic devices, and of course enjoying good times with my wife and children.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: is there any particular reason to use _CreateDataObject() in DataManager?memberMartin Cook15 Jun '04 - 15:27 
koo9 wrote:
but that's just for your demo purpose, otherwise it will keep instantiating class from your own implementation and never load other assembly.
 
I don't know what more I can say to convince you. If you are still skeptical then download the data access library from my other article, reconfigure the demo and step through the code in the debugger. Perhaps seeing will lead to believing.
 
koo9 wrote:
and also you user user_id as number in your cg_security_user table, do you intend to create mitlple profile for a single login name(e.g. login as admin or common user)? if so, I don't see the implementation of a miltiprofile login in ur lib.
 
The user_id uniquely identifies a user, but it doesn't place any constraints on how you use the library. Once a set of credentials is created for a user then any combination of roles may be associated with them - it is not necessary to create a separate set of user credentials for each role. If you want to modify the library to allow non-unique user names then all you really need to do is modify the constraints in the database. I'm not sure why you would want to take that approach, but you can if you like.
 

koo9 wrote:
sorry if I asked too many questions, I am currently looking at RBAC for my project with oracle db.
your article is the good example for simple RBAC implementation.

 
Oracle makes a fine RDBMS, and you can easily adapt this library for Oracle by implementing your own data access library. It is pretty easy to do.
 
Good luck with your project,
 
Martin.
 
Martin Cook
Jesus answered, "I am the way and the truth and the life. No one comes to the Father except through me." John 14:6
GeneralRe: is there any particular reason to use _CreateDataObject() in DataManager?memberkoo917 Jun '04 - 10:41 
I just check the config file, that prove me wrong on the reflection there.
 

GeneralUser Rights Bugmemberrparsons11311 Jun '04 - 11:04 
Not a bad thing but still kind of annoying. I saw your article and was ready to play with some security, but alas, I could not. Now that I have this and the other bug mentioned fixed however all is good in the land of me.
By the way I like this article.
 
In the CG.Security.Principal CustomPrincipal Class you need to add the following line of code to the CustomPrincipal Constructor that takes only a CustomIdentity... I put this at the bottom under the lookup for roles.
 

// Get the user rights.
m_rights = SecurityManager.EffectiveRights(identity.UserID);

 
Also while debugging I added a Method called GetAllRights. Below is that code.
 

public string[] GetAllRights() {
return m_rights;
}

 
If these steps weren't necessary then please help me to understand where the rights were suppose to be loaded...
 
Hope this helps.
GeneralRe: User Rights BugmemberMartin Cook11 Jun '04 - 15:02 
Yep, I messed up the code for the CustomPrincipal constructor while I was preparing everything for release. There are a couple of other goofs that I have located and fixed since I posted the article. I hope to have a new version posted ASAP.
 
I didn't expose the list of user rights in the CustomPrincipal clas since I didn't see the need. However, I did make the SecurityManager.EffectiveRights method public - so I suppose you could get the list that way if you really needed it.
 
I'm glad you are enjoying my library. Thanks for letting me know about the bugs.
 
Martin Cook
Jesus answered, "I am the way and the truth and the life. No one comes to the Father except through me." John 14:6
GeneralRe: User Rights Bugmemberrparsons11311 Jun '04 - 17:39 
Making the EffectiveRights method public has helped in looking at some things but it is also necessary to make the CustomIdentity.UserId a public instead of an internal, for lookups and such from the front end code.
 
Also I noticed in the demo app that you couldn’t assign Rights to Roles only Rights to users. Now that’s not a real problem because this is a demo app of the real project the security library so it doesn’t have to be a perfect working example. It sure would have explained why I was getting the right of Test1 though when I was doing a check on EffectiveRights though J
 
Anyways… Thanks for the article. I think I’ll dissect it some more until I can understand it.
 
Richard Parsons
1 Cor 9:16 “For though I preach the gospel, I have nothing to glory of: for necessity is laid upon me; yea, woe is unto me, if I preach not the gospel!”
GeneralRe: User Rights BugmemberMartin Cook12 Jun '04 - 5:14 
I have submitted an update for the article, along with new source for the demo and the library. Hopefully everything will be posted soon. The updates address all the problems you mentioned.
 
I have also submitted another article that explains the internals of the library and presents a data access extension for SQL-Server. If you are interested in this article then watch for the new one.
 
Thanks again.
 

 
Martin Cook
Jesus answered, "I am the way and the truth and the life. No one comes to the Father except through me." John 14:6
GeneralGreat Article / Code CorrectionmemberShayd11 Jun '04 - 8:42 
Great article.Big Grin | :-D
 
I did find a coding error in the demo program dealing with assigning rights to a user. The add (>) and remove (<) buttons were never being enabled due to the tab index it was checking for being incorrect in the function _UpdateGUI() as shown below:
 
m_buttonAddUserRight.Enabled = tabIndex == 1 && rightIndex >= 0;
m_buttonDelUserRight.Enabled = tabIndex == 1 && userRightIndex >= 0;
 
when changed to the following it works fine:
 
m_buttonAddUserRight.Enabled = tabIndex == 2 && rightIndex >= 0;
m_buttonDelUserRight.Enabled = tabIndex == 2 && userRightIndex >= 0;
 

Thanks again for some nice basic plumbing for roles and rights security.
 
Scott

GeneralRe: Great Article / Code CorrectionmemberMartin Cook11 Jun '04 - 15:04 
I noticed the same thing after I posted the article. I hope to have updated source files posted ASAP.
 
Thanks for letting me know about the bugs.
 
Martin Cook
Jesus answered, "I am the way and the truth and the life. No one comes to the Father except through me." John 14:6

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 4 Apr 2005
Article Copyright 2004 by Martin Cook
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid