views:

951

answers:

3

I'm working on a Rails web application, and it's currently being used by some 20 users.

Some parts of the application are only accessible by some users, so we already have a basic authorization framework in place, which I implemented using the acts_as_authenticated plugin.

The users' privileges depend on which department they work in, so for example administration has access to all parts of the application, while accounting only has access to the accounting-related parts, and sales only has access to sales-related parts, etc.

On the other hand, users see the links to actions for which they have unsufficient privileges. For example, those in the sales department see a link to the financial records in the main menu, but when they click on it, nothing happens. This is so because AFAIK there's no efficient way to query user privileges using acts_as_authenticated.

I want to change this in two ways:

  1. I want to introduce more fine-grained authorization. Currently, the authorization is done at the controller level. I want to do this at the action or model level. For example, I want those in the sales department to be able to create and update payments, but not delete them.

  2. I want to be able to query user privileges efficiently, so I can remove unnecessary (and confusing) links from the interface.

What do you think is the most elegant way to implement this?

Rails-specific answers aren't necessary, I just want to know how this should be implemented in a data-driven application.

Finally, here's how it's implemented currently:

def authorized?
  current_user.role.foo? or current_user.role.bar?
end

And here's my initial idea, which I think is not the best way to solve this:

+------------+------------+---------+
| department | controller | action  |
+------------+------------+---------+
| accounting | payments   | index   |
| accounting | payments   | new     |
| accounting | payments   | create  |
| accounting | payments   | edit    |
| accounting | payments   | update  |
| accounting | payments   | destroy |
| sales      | payments   | new     |
| sales      | payments   | create  |
| sales      | payments   | edit    |
| sales      | payments   | update  |
+------------+------------+---------+

or

+------------+----------+-------+--------+------+--------+--------+
| department | model    | list  | create | read | update | delete |
+------------+----------+-------+--------+------+--------+--------+
| accounting | payments | TRUE  | TRUE   | TRUE | TRUE   | TRUE   |
| sales      | payments | FALSE | TRUE   | TRUE | TRUE   | FALSE  |
+------------+----------+-------+--------+------+--------+--------+
A: 

you may need to introduce the notion of 'function points' or 'features' into your model as control-points for access; a 'feature' may have an optional 'parent feature' to form a hierarchy. You get to decide what is and isn't a feature, and check for permissions programatically. This should also allow you to check for feature-level access before drawing a menu, so that users never see links to pages they are not allowed to access.

a similar situation/solution is described here

Steven A. Lowe
A: 

Of your two proposals, the first option looks a bit better as it allows you to add actions which may not be model-level actions. I'm guessing you'll run into a case where the second model doesn't quite do enough, and you'll need to either change the schema or start sprinkling permissions logic throughout your app (ex. "Users without 'create' access also can't run method xxx")

I think the reason the solution doesn't look very DRY is that there's repetition:

  1. In the department names, and
  2. In the department capabilities

With regard to #1, it would make sense to create a departments table and give each department an Id.

With regard to #2, I agree with the first comment. You can probably cluster the various controllers and actions into functional groups, and then set a many to many relationship (mapping table) between users and functions. Functions would then have a one-to-many relationship with what actions/controllers they allow. This would let you, with minimal repetition, say things like "accounting and sales should be able to read all of the financial tables."

tlianza
I already have a departments table, and a users_departments table. I simplified the example table for brevity.
Can Berk Güder
+3  A: 

The basic concept of authorization, as I understand it, is a role. Role can express various things:

  1. relation of a user to the system as a whole (eg. to be an admin of the system)
  2. relation of a user to some kind of entities (eg. to be a moderator of comments)
  3. relation of a user to some particular entity (eg. to be an owner of some resource)
  4. some other complex relation (eg. to be a friend of a user that is a owner of some resource)
  5. that user has some attribute(s) or it responds to some message in some particular way (eg. to be a teenager)

A really fine grained authorization system should allow you to define role for a user based on any of the above mentioned criteria. Furthermore, it should allow you to set more than one role for a user. (The simplest forms of authorization plugins for Rails usually allow you define just the first kind of roles and set just one role for a user.)

The other part of authoriation is a mechanism that decides which part of code to run (or not to run) based on the fact if a user fits into some role (set of roles) or not. To apply this mechanism, we have to find the points where the authorization should take place and select roles for which the code should or should not be run.

The way that works for me in Rails is to define roles on the model level and to leave authorization mechanism (setting allowed roles for parts of code that I want to be authorized and asking if current user has the role that is permitted to run the part) entirely for controllers/views.

For this I use tweaked rails-authorization-plugin which has all the possibilities I just mentioned built right in (various kinds of roles, many roles for one user, authorization on controller and view level).

Milan Novota