10
\$\begingroup\$

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 statements.

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 exist 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 is that I 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.

\$\endgroup\$
3
\$\begingroup\$

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).

\$\endgroup\$
  • \$\begingroup\$ What happens when you want to disable features for a user? \$\endgroup\$ – jwize Jun 14 '14 at 6:36
  • \$\begingroup\$ You remove the operation from the user's list of roles \$\endgroup\$ – KRam Jun 15 '14 at 13:50
  • \$\begingroup\$ 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. \$\endgroup\$ – jwize Jun 15 '14 at 14:11
  • \$\begingroup\$ 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. \$\endgroup\$ – KRam Jun 15 '14 at 16:10
0
\$\begingroup\$

I have successfully used a design many times that I call the Feature Set Matrix. I use it since I have found Product Line Engineering to be overly complex for my situations.

For each major category of functionality in an application/product that you want to make permission- or license-based, identify it as a feature set. Examples: "Security" and "WebServices".

Within each feature set, identify one or more product options, which are individual actions/operations/functions. Examples for Security could be: "LDAP", "ActiveDirectory", "Biometric", and "Custom".

Depending on your needs, you can add the actions into your database like you have and/or assign them enum values.

Also, I usually have a User table, a Role table, and a User_Role (xref/join) table, and I assign the permissions to a Role instead of a User. I have found it makes user administration easier. I haven't used the Role Provider that is in the framework as KRam suggested. Not because I didn't want to, but because my management didn't want to.

\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review, clairestreb! This answer would be significantly better if you pointed out how your approach is different from the OP's approach. Right now, it sounds like you are suggesting pretty much the same thing as the OP uses. It is hard to see the differences. Perhaps you could point out the specific differences? \$\endgroup\$ – Simon Forsberg Sep 27 '14 at 20:33
  • \$\begingroup\$ 1) The OP is only using features, whereas sometimes I have found the need for feature sets with product options, which is similar to the OP's approach in that the OP's features equate to product options, but it is different because feature sets have bundled/categorized them into groups. Either way works, it just depends on the situation. 2) The OP states "Roles can have features, and Members can have features," whereas I have described that only roles have features, and Members have one or more roles. 3) The OP uses only enums, whereas I was suggesting to also store them in a database. \$\endgroup\$ – clairestreb Sep 29 '14 at 14:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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