Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Feature Based Authorization

It seems to me that if you rely on roles to authorize a web application it makes it very difficult to render UI or code based on a set of features. This means that each role would have be checked against for each feature.

if(User.IsInRole("SomeRole") || User.IsInRole("OtherRole")) { // Do something } // C#
@if(User.IsInRole("SomeRole") || User.IsInRole("OtherRole")) { // Do something }  // Razor

This seems very rigid. It is for this reason that I started to think of the application in terms of features. Roles can have features and Members can have features. Next the features have to be authorized based on the Member's current feature-set.

if(SomeFeature) { // Do Something } // In C#
@if(SomeFeature) { <h1>Show Something</h1> } // In Razor

Members Have Roles

ASP.NET/MVC provides a framework to manage roles using sql server (aspnet_regsql). We are using this out of the box and things are working fine so far.

Roles Have Features

In order to have more granular authorization we are attaching features to roles in a many to many reference table for Features and aspnet_roles and Feature to aspnet_users tables in the same fashion.

Wrap Everything in A Feature When Authorization is Needed

Everything that needs to authorized is wrapped in a feature. Although, my heart would like to believe in a more concise method I can't fathom this would be possible.

Wrap on the Server

Conditional statement for code that is only available to this feature.

if(Feature.Authorized(Features.SomeFeature))
{
    // Code in here that processes the feature ... 
}

Wrap on the client

Same as above but markup is wrapped up for the feature.

@if(Feature.Authorized(Features.SomeFeature))
{
    <h1>Markup if feature exists.</h1>
});

Class to Check Authorization.

The account service is called to authorize the feature for the previous conditional statments.

public static class Feature
{
    public static Boolean Authorized(Features features)
    {
        var service = NinjectWebCommon.Kernel.Get<IAccountService>();
        return service.IsAuthorized(Enum.GetName(typeof (Features), features));
    }
}

Enum With Features

This is a set of features that can be updated at design time to add a feature.

public enum Features
{
    SomeFeature
}

Classes to Create and Find if Features Exists Account Service con't

The provides a set of methods to interact with features in the database.

IsAuthorized Method

Checks to see if this user has the given features. This helps with rendering or executing conditional code or UI. The bonus here i the add feature to the database if it doesn't already exist. This means a developer can go about his/her work without any configuration aside from adding a Features enum entry and then authorizing against it.

    public override Boolean IsAuthorized(String name)
    {
        // Find the feature in the database. 
        // Add the feature to the database if not found. 
        var feature = 
            db.Features.SingleOrDefault(f => f.Name == name) ?? 
            db.Features.Add(new Feature()
            {
                Name = name,
                PageUrl = Request.Url.AbsolutePath
            });
            db.SaveChanges();

        // MyFeatures holds a list of features available to the current user
        // The feature set is queried as requested.
        // The features are a union of features assigned to the user as well as 
        // features assigned to Roles that the current user.
        return MyFeatures.Contains(feature.Name);
    }

Current Features Available

Gets or creates a new list of features based on features and roles in the database.

    public List<String> MyFeatures
    {
        get
        {
            var session = HttpContext.Current.Session;

            // Get cached features or from stored procedure call.
            // The stored procedure is of the UNION of features stated earlier.
            var result = session[Constants.Features.Key] as List<String> ??
                (session[Constants.Features.Key] = GetCurrentFeatures());

            return features;
        }
    }

Gets Features From Database

    private List<String> GetCurrentFeatures()
    {
        var user = Membership.GetUser(HttpContext.Current.User.Identity.Name);
        var features = from f in Methods.GetCurrentFeatures((Guid)user.ProviderUserKey)
                       select f.Name;
        return features.ToList();
    }

This is my design but I was wondering if there are any flaws with this or Best Practices that I haven't thought of.

share|improve this question

1 Answer 1

Couple of items that might need a re-look:

  • The now deprecated AzMan has very similar concepts. The conceptual equivalent for the 'Feature' in your application is an 'Operation' in AzMan. I'd suggest taking a look at it to understand how an application can use role based access to get the granularity that is desired.

  • A Custom Role Provider can help you achieve the same effect. You could call the User.IsInRole() method, but pass in the Feature name. In your custom role provider, you can perform the above specified IsAuthorized() logic. That allows you to support a role hierarchy where Roles in the traditional sense are made up of Operations (or Features). If a user is granted a role, it means he can also authorized for all the underlying operations (features). Ex: an 'Admin' Role who can perform 'UserAddition', 'UserModification' and 'UserArchival' features. All you'd need is an additional table in your database which stores the hierarchy and a lookup to generate the grand feature capability list.

  • The advantage of using a Role Provider is that it is supported in the framework, already has a plugin model, it has a defined place in the Callback Lifecycle and can be swapped out without touching any other parts of the system.

  • Using the IOC resolver directly in usually not done as it leads to additional dependencies in Unit Testing and is not easily mockable and it also adds a hard dependency on the particular IOC framework. One alternative would be to make an extension method that can then source the dependency from the extended object (such as Page.User).

share|improve this answer
    
What happens when you want to disable features for a user? –  jwize Jun 14 at 6:36
    
You remove the operation from the user's list of roles –  KRam Jun 15 at 13:50
    
I am not sure I follow what you mean by that. You mean you remove a operation from a role? If so that would mean every user would lose that feature. –  jwize Jun 15 at 14:11
    
If you are going to use a two level hierarchy of a parent feature group (I refer to this as role) and a child feature ( I refer to it as operation), if you want to suppress certain features, you can do it in two ways: 1. If you have to suppress certain features multiple times, create a different feature group which has those features disabled and assign users that feature group 2. Remove the parent feature group and add all the underlying child features except the ones you want to suppress. –  KRam Jun 15 at 16:10

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.