Mar 9 2009

Why do I need your Permission?

We have all seen an Authorization pattern like this at least once, and if you have been in the business for a while you have seen it more often then you probably care to admit:

bool isAdmin = User.IsInRole("Administrator");

Every time I see this it leaves me speechless, except this time. I just don't understand why people continue to follow patterns like this, I mean really, its madness. In security circles, the above call is a Branched security call, which is fine as a pattern, but to tie oneself or an application to such broad permission groups such as Administrators, Users, Power Users, etc. is just asking for trouble. There are more appropriate ways to architect security into an application and I believe they give you more flexibility, are testable in isolation, can be easily extended, and can survive inevitable changes in system/domain security infrastructure.

My basic theory is that the problem relates to how the developers and 'The Business' communicate what they want.  I can just image the business story which probably goes something like this:

Users should be able to do some things and prevented from doing others.
- The Business

Like many business requirements this is so broad you can drive a truck through it, so the name of the game in cases like this is to code for flexibility. You can still have a thing called Admins or Users but under the covers we need to be more discreet and more flexible, because "The Business" will almost certainly change their mind. The questions beyond this point are very similar to those asked for other development initiatives.  Most of these are rhetorical for this post, but the last one requires some derived requirements to frame the samples.

  • What are/should be the areas of concern? 
  • How fine should the controls/configuration be for each area? 
  • What layers will I need to provide the requisite flexibility, testability, etc?
  • What architectural tenets and/or standards should guide our design?
    • The application SHALL not be tied directly to any one Authorization source.
    • Assessments for Authorization SHALL be made against a Principle, although the design SHOULD not prevent adding support for contextual assessments at some later date.
    • The application layers SHALL be testable in isolation.

Note: The use of the terms SHOULD and SHALL are guided by RFC 2119. If you find yourself writing requirements formally or otherwise this is worth a read. 

When I review the requirements it leads me almost immediately to a pattern prescribed by Code Access Security (CAS), namely Permissions (declarative and/or imperative). Wait, wait..! Before you panic, I do not mean to say we need to plug in to the full CAS foundation, but there is a lot of merit; relative to our needs; to the CAS Permission architecture. In our case, it will seperate areas of concern for Authorization and will give us a great deal of flexibility moving forward.

Lets start by writing a base class which will allow us to reuse a lot of the code used by each of our Permissions.

using System;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;

public abstract class PermissionBase
    : CodeAccessPermission
{
    protected PermissionBase()
        : this(PermissionState.Unrestricted)
    {
    }

    protected PermissionBase(PermissionState state)
    {
        // Instance of the restriction flag which indicates 
        // whether or not unrestricted permission is required
        Unrestricted = state == PermissionState.Unrestricted;
    }

    public bool Unrestricted { get; private set; }

    // Abstract method to Perform a security demand to determine if the user has
    // the specified role
    public abstract new void Demand();

    // Performs a security demand to determine if the user has the specified role
    protected void DemandPrinciplePermission(string roleName)
    {
        try
        {
            AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
            new PrincipalPermission(null, roleName, true).Demand();
        }
        catch (SecurityException ex)
        {
            throw new MyCustomSecurityException("The user is not a member of a the role required to hold this permission: " +
                                roleName, ex);
        }
    }
}

Here are a few key things to note:

  • Assessments for Authorization are being made against the Principle for the current thread. If for some reason we need to change this in the future or supplement it with other factors from other sources, we can do so without affecting other layers. This is a very common practice in the security world and provides us with a lot of flexibility. While it will mean you need to set the thread Principle on each thread, that is a separate area of concern and can be done easily. In another way, this will give you flexibility as well, just give it a think and it should become clear why
  • The Unrestricted property is not germane to our implementation, but if you read a little more into CAS you can see how to take advantage of this pattern
  • The code for the MyCustomSecurityException is not shown but it is just a custom exception class that inherits from SecurityException. I like using this pattern because it makes it easier to add details to the exception message, but have it still be caught as a SecurityException
  • The pattern employed here is, make the assessment, if the user has the permission do nothing. If the user does not, throw an exception and end the current line of execution. Again, this is a standard practice in the security world. Avoid making this a true/false operation, that will lead to mistakes and create vulnerabilities in your product
  • There are a few other members in the base class (e.g. CodeAccessPermission) which need to be implemented, but for our purposes they can just throw the NotImplementedException since we will not be using them

Next we need an implementation of a Permission representing a capability we desire to Authorize or block access to. Notice that I said "capability" and not "Group". The example we started with implies that we are testing for Group membership (e.g. Administrators), but I think this is the wrong way of looking at things. What we are really trying to do is assess whether or not someone (or thread) should be allowed to use a discreet capability (Ex. Comment a record) regardless the group they may or may not be part of. The caller should never have any notion of "Group" or "Role", they should just understand they need to ask for permission first. If we create permissions around capabilities we can mix and match capabilities within groups as needed. This pattern also allows us to create Permission Unions and/or Intersections to create complex Permission scenarios. Again, LOTS of flexibility.

public sealed class CommentRecordPermission
    : PermissionBase
{
    internal const string CommentRecordRoleName = "CommentRecord";

    public CommentRecordPermission(PermissionState state)
        : base(state)
    { }

    public override void Demand()
    {
        DemandPrinciplePermission(CommentRecordRoleName);
    }
}

Here are a few key things to note:

  • The internal const is a simple representation of how we abstract the caller from understanding the underlying assumptions related to how permissions are granted. In practice, they can be bound to configuration files, Directory Service group names, Role-Based Security (RBS) Role names, etc. If you use a token to define a permission then it is useful to expose it as a const so you can use the const to make Branch calls later (Ex. User.IsInRole(CommentRecordPermission.CommentRecordRoleName)). This is for cases where you want to make a simple assessment without generating an exception, but beware, you are asking for trouble

Using the permission is pretty straight forward.

CommentRecordPermission permission = new CommentRecordPermission(PermissionState.Unrestricted); permission.Demand();

The previous example represents an imperative security call. This pattern allows you to respond more directly to exceptions but means a method is actually entered to make the call which could lead to vulnerabilities if you do not handle the exception correctly. Alternatively you could create a Permission Attribute. Here is an example:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Struct | AttributeTargets.Class | AttributeTargets.Assembly,
AllowMultiple = true, Inherited = true)]
public abstract class PermissionAttributeBase
    : CodeAccessSecurityAttribute
{
    protected PermissionAttributeBase()
        : base(SecurityAction.Demand)
    {
        CreatePermission();
    }

    protected PermissionAttributeBase(SecurityAction action)
        : base(action)
    {
        CreatePermission();
    }
}

public sealed class CommentRecordPermissionAttribute
    : PermissionAttributeBase
{
    public CommentRecordPermissionAttribute(SecurityAction action)
        : base(action)
    {
        if (action != SecurityAction.Demand)
            throw new NotSupportedException("This attribute only support 'Demand' security actions");
    }

    public override IPermission CreatePermission()
    {
        CommentRecordPermission permission = new CommentRecordPermission(PermissionState.Unrestricted);
        permission.Demand();

        return permission;
} }

Using the attribute is even easier than using a Permission. Keep in mind that the CommentRecord method will not be entered unless the user meets the permission constraint. The down side is the caller needs to ensure Security Exceptions are handled.

public class Foo
{
    [CommentRecordPermission(SecurityAction.Demand)]
    public void CommentRecord()
    {}
}

Ok, I know... this is a lot more code. But if you go re-read the beginning, I never said it would be less code, I just said it would be more flexible and would help keep you out of trouble. If you take the time to digest this, you may begin to see what this can do for you. Also, if you go revisit the questions/requirements identified at the beginning, we now have an architecture that meets all stated goals.

Tags: ,

Comments are closed