سؤال

This is currently my User-Role-Permission model in the Db schema (excluded other tables to avoid confusion). My implementation goes like this: I have a Customer table and each Role has a different set of permissions to enable the User to interact with it. The User with Salesman role can only read from it while Admin has all the CRUD permissions. To do this I'll save 4 entries in the Permission table with perm_type = "create customer", "read customer" etc. I have a clear mapping of which role has which permissions and those can be changed dynamically as well; simply insert or delete entries in the Role_Permission table. So before performing any action I'll just check the user role and whether the role has the required permission with a simple if statement in my code. enter image description here

All good so far but there's a catch now. The Salesman is supposed to have permission to update Customer as well but only the balance field. Salesman cannot update anything else say Customer name, contact etc. There are 2 ways I can implement this:

  1. Grant update permission to Salesman but in the client side only display the form for updating balance. But then again someone can exploit this, right?
  2. Create 2 methods and their corresponding permissions in the Permission table: updateCustomer(Customer updatedCustomer) and updateCustomerBalance(int id, float balance). But what if in future I want salesman to be able to update contact or address of Customer? I'll have to add more methods for that, compile and deploy my code again. I can do that for every field before deploying my app but it looks very cumbersome especially when I have at least 15 more tables.

So is there any other way I can do this?

هل كانت مفيدة؟

المحلول

Its generally better to provide fine grained permissions, this makes it possible to change the exact set of information and actions available to a given user. But as you've noted this means being uber specific.

One solution is to order your permission names hierarchically, and use a matching technique like prefix matches, or full regexs.

ie:

 Customer.Name.View
 Customer.Name.Update
 Customer.Address.Street.View
 ...
 Customer.Balance.Update
 ...

Now assign the permission to the role:

 (SalesPerson, Customer.Balance.Update)
 (SalesPerson, Customer.*.View)
 (Accounts, Customer.*)

You will authorise like this:

 if (LoggedInUser.HasPermission("Customer.Balance.Update"))
 {
      ...allow action...
 }

HasPermission() would need to compare each element of the path against the permitted paths. When it encounters an * this will match zero or more path elements.

eg:

   Customer.Balance.View

   matches:
       *
       *.View
       Customer.*.View
       *.Balance.*
       Customer.*
       Customer.Balance.View

   but does not match:

       *.Update
       Sales.*
       Update.Update

You could do this using loops, and backtracking. A more efficient approach would be to use an Finite State Automata. Or to make use of the databases own text matching mechanisms, but the basic idea is there.


It might also serve to split the permission into two more parts.

  1. The name of the entity set being controlled
  2. The actions being permitted

Think FileSystem.

eg:

 Customer.Name
 Customer.Update
 <Noun>

 View
 Update
 Execute
 <Verb>

This would make permissions slightly more complex, but still simple:

 (SalesPerson, Customer.*, View)
 (SalesPerson, Customer.Balance, Update)

 (<Who>, <What>, <Action>)

And authorisation would then be:

 if (LoggedInUser.HasPermission("Customer.Balance", Update))
 {
      ...allow action...
 }

Even better the Action is probably not going to change much, so you might get away with a bitset. Each action being represented as a bit being on or off. This allows compound Actions like say the user not only needs Update, but also Execute (or some other Action). thus giving:

 if (LoggedInUser.HasPermission("Customer.Balance", Update | Execute))
 {
      ...allow action...
 }

or

 Write = Update | Execute;
 ...

 if (LoggedInUser.HasPermission("Customer.Balance", Write))
 {
      ...allow action...
 }

Much clearer intentions.


Final thought with this scheme: Allow genuine object identifiers, and selectors.

This could be a GUID, a path.id, a path(id).path, or even a specific user:

 (Bob, SalesPerson)
 ...
 (SalesPerson, Customer.Name, View)
 (Bob, {B3E7A668-8645-495D-A784-2CD0B120E91D}, Admin)
 (Bob, Opportunity.2436548, Owner)
 (Bob, Opportunity(4563458).Address, Owner)
 (SalesPerson, Customers(DaysOld<30), View)

Bob is a SalesPerson, but also admins for whatever that is, and Owns a specific opportunity (among other privileges), even though otherwise he has no rights to those things through any other role.

The checking would be more complex though. Depending on how you construct the paths and what sort of selectors you supported. It would make sense to change how checks are performed:

  var permit = LoggedInUser.Permit("Customer.Balance", Update);
  if (permit.Invalid())
  {
       ...reject the user here, there aren't even maybe rules...
  }
  var Customer = ...Obtain the Customer Object...
  if (permit.valid(Customer))
  {
       ...perform action here on that Customer...
  }

The Permit is created by selecting all permissions that could match, even if there isn't enough information yet. eg:

  (Customer.*, Update)
  (Customer.Balance, Update)
  ({B3E7A668-8645-495D-A784-2CD0B120E91D}, Update)
  (Customer(DaysOld<30).Balance, Update)

The next check is too see if any permission were returned at all in the permit. An empty permit is by definition invalid.

Happy that the user, might be permitted, we load the customer. Then tell the permit about which specific customer we want to operate on. As there may be many such checks, permit returns a copy with the still valid subset.

We then check this refined permit to see if it is still valid. If so do the work, otherwise the customer didn't satisfy any of the candidate permissions, so reject it.

نصائح أخرى

A role is set of functionality grouped as one like in your case SALESMAN what a salesman can do is driven by permissions, so permission define what functionality is allowed or not.

Basically now there is user which is performing SALESMAN role for which user gets certain functionalities based on permissions which it can perform. Now when a user with SALESMAN role is asked to perform additional functionality like MANAGE_CUSTOMER..then logically another role is getting assigned to that user the additional role will have own set of permissions.

Next part is granularity of permissions at Entity (CUSTOMER) level, a User might have view permission but doesn't have edit permission or like in your case editing of certain fields. There are two approaches that I can think of for handling this.

  1. Assign attribute level permissions (VIEW/EDIT) to the new role.

  2. Create a separate table with entity name and attribute list (named entity_attributes),add another table which map role to (entity_attributes) table and add generic permission (VIEW/EDIT) for each row. Now on logic side compare list of all incoming attributes for editing with list of attributes allowed as per these tables.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى softwareengineering.stackexchange
scroll top