9

I have been tasked with writing an on/off control for features in our product based on who is signed in, in principle with one on/off flag for each feature. Put simply, this is a permission based feature access that can be activated at the user level.

The problem is that I can see this becoming a complexity nightmare. For example, person A is apart of group G but has special per missions 1, 2, 3 but A is completely different than person B who is apart of Group H who has special permissions 2, 3 and 4 etc etc.

Managing the permissions is not going to be the hard part : that is just setting a value in the database. The hard part is going to be allowing person A to be able to use feature F if they have permission P. I do not want this to become hundreds of if statements scattered across the code base whose combinations are exponential.

One solution that I have in mind is to separate the feature from the permission by having a control layer right above the feature that everything else uses, similar to writing a library and using a scripting language for the control structures. This would separate the if/else statements from the actual feature, but the problem is that they still exist, and their combinations are still there.

Do you know of any elegant solutions to this problem or ones similar to it?

Christophe
  • 74,672
  • 10
  • 115
  • 187
zzelman
  • 219
  • 2
  • 3
  • Why do you think you're going to have combinatorial situations? – whatsisname Mar 28 '17 at 17:07
  • 1
    **@whatsisname** Because the permissions and/or features are not guaranteed mutually exclusive. I can't say for certain how intermixed they are, that would be done on a case by case basis, just that the possibility is not prevented. – zzelman Mar 28 '17 at 17:16
  • Very very very related (but a fraction of an inch from being a duplicate): [Storing menu items with user permissions](http://softwareengineering.stackexchange.com/questions/206388/storing-menu-items-with-user-permissions). – Greg Burghardt Mar 28 '17 at 18:13
  • This might be too broad to answer right now. Can you provide some specifics about your technology stack? Language? Frameworks? – Greg Burghardt Mar 28 '17 at 21:47

3 Answers3

4

1.Organize the permissions and their control

Indeed, if you define individual feature access permissions, you will have, as in feature toggles, an if-clause in each feature to check if the user has the right permission.

If you abstract the feature from the permission, for example by using a level of indirection, such as a feature group (e.g. one feature belongs to a configurable group) or an autorisation profile (e.g. several user belongs to one profile, which has several feature permissions), you will still need an if clause for each feature.

In order to make this easy to handle, you should in anyway encapsulate the permission check in a permission object. This would allow you to implement the link between the feature and the user with any of the above mentioned strategy, without having to change the code:

user.permission("feature_X").mandatory_check(); // if needed, throws a permission exception 
                                                // to be catched at right level. 

or

if (user.permission("feature_Y").optional_check() ) {
   ...                                         // execute optional feature 
}                                              // do nothing if access is not granted

2.Control features access upfront

If the features are independent functions, you could control access upfront.

This means that the UI shall ensure that user can only invoke fonctions and commands for which the user has permission. This allows to isolate permission controls in your architecture. The various feature will then not have to implement additional checks.

Unfortunately, this will not work if the feature is optional, i.e. the user can invoke a function, and the function is executed differently if the feature is there or not.

Manage features as a strategy

This approach uses runtime strategies. The strategy either invokes a feature or nothing. The Context would in this case be a kind of user profile object that is initialized at login. For each feature there would be a strategy (this could remain flexible, by using an associative container such as a feature map). The strategies would be initialized depending on the user permissions.

Later on, when the different functions of the software are executed, they would use the relevant strategy (e.g. either an object that will execute the right feature, or an empty strategy object that does nothing).

This approach seems rather elegant. However, instead of adding an if in the code of each feature, it would require to implement each feature as a self-contained strategy, which might be extremely challenging.

So what ?

Personally, even if the second seems more elegant, I'd go for option 1: this is much more flexible. Proper encapsulation of the permission can reduce the permission control in the feature to a single statement.

Christophe
  • 74,672
  • 10
  • 115
  • 187
3

There are a lot of different ways to do permissions, but here are some pointers.

Bear in mind there are two programs here: the administrator interface, which lets an admin set up users, groups, and permissions; and the main program, which needs to know who has which permissions.

Start with the main program

Permissions get set up only occasionally. But they get read over and over. Reading is far more important in this regard.

You will end up adding this sort of code to nearly every page/form:

bool ok = HasPermission(user, permissionCode);

and sometimes

AssertHasPermission(user, permissionCode);

or maybe

AssertHasPermission(user, permissionCode, Action callbackIfFailed);

the latter could be useful if called like this:

AssertHasPermission(context.User, "108", () => context.Redirect("~/Error.html"));

You want this code to run very efficiently. Whatever data structure you use, it must be simple; take a user and permission and see if it exists. I suggest a flat/denormalized structure. At run-time, it should not be necessary to walk a tree, for example, or apply cascading rules.

If your requirements include group membership and group permissions, implement those later... see below.

A feature might not be a feature

To an end user, a "feature" might be composed of several functions in code. For this reason, you need at least one layer of indirection between a permission and a code base. That is why in the example above I am passing a permission code and not, say, a page name. You will need this abstraction in case you need to add more pages to a feature, or if a page has to be split up into several features (e.g. read and write for the same domain object).

Groups, cascading permissions, and add/subtract rules

Sounds like your requirements include group permissions. This sort of thing is almost cosmetic-- it is mainly to make things easier for an end user, i.e. manage several users in lockstep by putting them in a group. But then there are exceptions; maybe you need the ability to override group permissions for certain members, for example, or you need two different permissions to access a single feature. You can drive yourself mad with the options.

The important thing is-- keep these options out of the main code base. Your main code shouldn't care if a user is in a group; it only matters if he has the right permission code after all group rules are applied.

I would suggest that, after accepting input from the administrator, the admin program should maintain a separate structure for groups and users, then walk the structure and compose the flat structure that will be used by the main program. You can think of this as "compiling" or denormalizing.

If you do this correctly, you will save yourself a lot of work. Later on, if your concepts of group membership and group permissions change (e.g. you add blacklisting as well as whitelisting, or you integrate with Active Directory groups, etc.), the computed permissions list should still remain the same old flat structure in the end. That way you can tweak the admin UI to your heart's content and your main code base won't require any modification.

YAGNI

In my experience, people get very creative when coming up with possibilities for managing permissions. In almost every single case I have seen this, the resulting implementation is way more complex than is actually needed. Keep things simple. Don't try to include every imaginable use case. Instead, focus on extensibility, and remember you can always improve the group structures without impacting the main program.

John Wu
  • 26,032
  • 10
  • 63
  • 84
1

A very common approach that typically requires a small number of combinations is role-based access control. In RBAC, you'd give each resource a name, possibly in a hierarchy. You then create roles with permissions to perform certain verbs (like "create", "read", "list", "watch", "delete", "execute") on those resources. Then you assign subjects to those roles.

Let's say for your example that your permissions mapped to:

  1. Create new customer
  2. Read customer info
  3. Read logs
  4. Execute self test

You might create a "Customer Service" role with permissions 1-3, and a "Technician" role with permissions 2-4, then assign those roles to subjects as appropriate. You could assign both of these roles to a manager.

Your code for reading logs then just has to check if the current subject is assigned to a role with "read" permission on the "log" resource.

This allows for a flexible mapping from user roles to groups of permissions that you don't have to guess right at the start, and doesn't have to be the same for every customer.

Karl Bielefeldt
  • 146,727
  • 38
  • 279
  • 479