Having looked through quite a few existing Rails authorization plugins, we decided, we were in need of a different approach. Mainly, it was the missing separation of authorization logic from business logic in the evaluated plugins that caused us to implement a new plugin, declarative_authorization.
In our declarative approach, authorization rules are grouped in a policy file, while only privileges are used inside program code to enforce restrictions. We developed for flexibility and simplicity, requiring only very simple statements in rules and program code. So instead of
class ConferenceController < ApplicationController access_control :DEFAULT => [:admin], [:index, :show] => [...], [:edit, :update] => [:admin, :conference_organizer] end cond = permit?([:admin, :conference_organizer]) ? {} : {:published => true} Conference.find(:all, :conditions => cond) <% restrict_to [:admin, :conference_organizer] do %> <%= link_to 'Edit', edit_conference_path(conference) %> <% end %>
with all the authorization logic interweaved with your code, you only need this
class ConferencesController < ApplicationController filter_access_to :all def index @conferences = Conference.with_permissions_to(:read) end end <%= link_to 'Edit', edit_conference_path(conference) if permitted_to? :edit, conference %>
And, separated in one place the authorization rules:
role :guest do has_permission_on :conferences, :to => :read end role :conference_organizer do has_permission_on :conferences, :to => :manage end
So, the same rules are used in enforcing authorization in Model, View and Controller. Also, they are used for Query Rewriting to automatically constrain the retrieved records according to the authorization rules. Thus, you just modify the rules on authorization requirement changes and you can also use the rules to talk to business owners of Agile projects.
For additional information and more examples, refer to the README and the rdoc documentation. Currently, we are using the plugin for an application with fairly complex authorization and it will be taking into production in the next iteration. So, look into it if you have authorization concerns in your application, it’s released under MIT license.
[...] I’m in Berlin for RailsConf Europe currently where I’m talking together with Carsten Bormann about implementing application security in Agile development with Rails and announcing declarative_authorization. [...]
Posted by steffenbartsch — From Rails Security to Application Security on September 4th, 2008.
Hallo,
ich habe eine kurze Frage betreffend der Installation:
* Add roles field to User model: a migration and table for roles,
+has_many+ :+roles+ in User model and a roles method that returns the roles
as an Array of Symbols, e.g.
def roles
(super || []).map {|r| r.title.to_sym}
end
Ich denke man legt eine Tabelle roles an mit den Attributen id:int und title:string.
In der Tabelle user könnte man per roles_id den Benutzer mit der Rolle verknüpfen.
Ich verstehe jedoch nicht die :has_many Beziehung – muss ich auch eine Tabelle für die m:n Beziehung anlegen? Ich frage, da es nicht erwähnt ist.
Gruß Torsten
Posted by Torsten on September 8th, 2008.
Hi Torsten,
you are right, there are no details on how to implement the user-roles association in the README simply because there are a few options depending on your application. In the simplest approach you won’t even need a roles table but would employ the
serialize :roles, Array
statement in your user model. I would prefer to have a roles table mapping user_id to roles.title. This is not fully normalized but allows multiple roles per user and seems maintainable enough for many contexts.
The has_many statement would declare roles.user_id to be the foreign key pointing to the user object. There is no join table involved here, just a_user.roles pointing to rows of (user_id, role_title) in table roles.
HTH, Steffen
Posted by Steffen Bartsch on September 8th, 2008.
Hi Steffen,
sorry but I have to ask again. Now I have the roles table with roles.user_id and roles.title
# role.rb snippet
class Role < ActiveRecord::Base
belongs_to :user
def roles
(super || []).map {|r| r.title.to_sym}
end
end
# user.rb snippet
class User < ActiveRecord::Base
has_many :roles
…
end
class ActivitiesController < ApplicationController
filter_access_to :all
…
end
authorization do
role :guest do
has_permission_on :activities, :to => :read
end
role :admin do
has_permission_on :activities, :to => :manage
end
end
privileges do
# default privilege hierarchies to facilitate RESTful Rails apps
privilege :manage, :includes => [:create, :read, :update, :delete]
privilege :read, :includes => [:index, :show]
privilege :create, :includes => :new
privilege :update, :includes => :edit
privilege :delete, :includes => :destroy
end
In the table I created a record with
user_id=3, title=”admin”
As long as I am a guest (not logged in) it works, but after login as admin user the system don’t allow the access to my actions in activities controller. Do I miss anything at the roles method in role.rb?
Best, Torsten
Posted by Torsten on September 8th, 2008.
Hi Torsten,
the problem is the location of the roles method. It is meant to override the User.roles method. Just move the roles method to the user model.
Steffen
Posted by Steffen Bartsch on September 9th, 2008.
I am assuming that is not meant to happen?
if I have filter_access_to :destroy
lets say for a comments controller,
and I have
role :admin do
has_permission_on :comments, :to => :delete
end
then I normal user should be able to create a comment, but the before filter that is created wants a access rule for create
“Permission denied: No matching filter access rule found for comments.create”
when I specify destroy thats the only action I want to filter access to
Richard
Posted by hookercookerman on September 15th, 2008.
Hello Steffen,
I’m quite new to ruby, rails and declarative_authorization. Everything is working fine so far but my application is growing up a little bit and I’d like to know if there is any way I could DRY up my authorization rules to configure multiple contexts at once, like this:
role :conference_organizer do
has_permission_on [:conferences, :presentations, :speakers], :to => :manage
end
TIA, João
Posted by João Augusto on September 16th, 2008.
Hi Richard,
the fact is, I had a default-deny policy in there — just to be sure. Obviously, when setting it, this use case didn’t occur to me. On second thought, you are certainly right, though. I changed that to a default-allow as this is what comes to less of a surprise to the developer. It is on the github master branch by now. Thanks for pointing this out.
Steffen
Posted by Steffen Bartsch on September 16th, 2008.
João,
that’s a nice optimization of the authorization rules syntax and required only minimal code change. I just pushed your suggested change to github.
Steffen
Posted by Steffen Bartsch on September 16th, 2008.
I love the plugin, but ran into a little bit of a complicated issue. I understand the structure of the roles table well enough, but not actually what I should be storing there.
For example, your code for the overriden roles method creates an array of symbols for the different roles, and I’m wonder what the “title” field should contain. Is it something like:
* admin
or
* admin_on_projects
I guess what I’m asking is, how do I define context? What’s the purpose of the array if all it’s going to contain is a bunch of the same symbols? Am I missing something?
We’re creating site that allows for multiple projects per user account. A user may have different roles on each project, so what should my role table entries be? “project_1_admin”, “project_2_user” ?
Thanks!
Posted by Mike Larkin on September 19th, 2008.
Looking through the tests, I see options for :context, but no examples on how to use it, or what the “title” should be in the database?
Thanks!
Posted by Mike Larkin on September 19th, 2008.
Sorry to keep posting, but I noticed a bug regarding pluralization. I have a People model, and permissions like
has_permission_on :people, :to => :manage
I’m getting a permission denied…
:context => :peoples
Posted by Mike Larkin on September 19th, 2008.
I’m attempting to use the advanced permission rule to allow the current_user to :manage their own message, but only :read everyone else’s.
In my case I am using a Person model instead of User.
However, the current_user always gets the manage permission
has_permission_on :messages, :to => :read
has_permission_on :messages do
to :manage
# user refers to the current_user when evaluating
if_attribute :person_id => is {user.id}
end
I’ve also tried:
if_attribute :person => is {user}
if_attribute :person.id => contains {user.id}
if attribute :person => is {person}
but none of these actually deny the manage permission. What am I doing wrong?
Thanks!
Posted by Mike Larkin on September 19th, 2008.
Hi Mike,
the role would be “admin” for an overall admin and “project_admin” for one limited to assigned projects. For restricting access to the assigned projects, use the if_attribute context restriction. E.g.
role :project_admin
has_permissions_on :project, :to => :manage do
if_attribute :admin_users => contains { user }
end
end
Provided, that your Project model has a project#admin_users collection (has_many :admin_users, :through …).
Does this help?
Steffen
Posted by Steffen Bartsch on September 19th, 2008.
Mike,
on your question concerning the error on PeopleController, I just checked a fix into github to prevent the unnecessary second pluralization. That should fix it.
Steffen
Posted by Steffen Bartsch on September 19th, 2008.
> Looking through the tests, I see options for :context, but no
> examples on how to use it, or what the “title” should be in the
> database?
:context options just directly set the context for the authorization evaluation, see the rdoc documentation for more information on that.
I suppose with “title” you are referring to the Role model proposed in the README. Here, title is just the name of the role as specified in the authorization rules.
Steffen
Posted by Steffen Bartsch on September 19th, 2008.
Mike,
> I’m attempting to use the advanced permission rule to allow
> the current_user to :manage their own message, but only :read
> everyone else’s.
>
> In my case I am using a Person model instead of User.
>
> However, the current_user always gets the manage permission
> has_permission_on :messages, :to => :read
> has_permission_on :messages do
> to :manage
> if_attribute :person_id => is {user.id}
> end
has_permission_on :messages, :to => :read
has_permission_on :messages do
to :manage
if_attribute :person => is {user}
end
is perfectly correct. I have similar code working here. Could you verify that
message.person != person_object_of_the_request
for the request? Also, does it help to remove the second has_permission_on statement? In the rails console, you could also try:
engine = Authorization::Engine.instance
engine.permit!(:manage,
:context => :messages,
:user => person_object_of_the_request,
:object => message_object)
to drill down on the problem.
Steffen
Posted by Steffen Bartsch on September 19th, 2008.
Steffen — Really digging your security implementation here. Nicely done.
using_access_control is really cool, but for me, it can get in the way at times. (For example, when working with objects in the console, or setting up that all-important first admin user.) To help with this problem, I made a couple of changes to authorization.rb.
Modified one existing method:
# Modified this method to return @@ignore_access_control without also checking RAILS_ENV. This
# was done to allow us to switch access control on and off as needed.
def self.ignore_access_control (state = nil) # :nodoc:
@@ignore_access_control = state unless state.nil?
@@ignore_access_control
end
And added one new method:
# Executes a given block, bypassing all access control. Useful for legitimate cases where access
# control "gets in the way" (e.g., bootstrapping that first admin user, console work).
def self.without_access_control( &block )
state = self.ignore_access_control
raise AuthorizationError unless self.ignore_access_control( true )
yield
self.ignore_access_control( state )
nil
end
It’s probably not a very good solution (I’m definitely NOT the expert here), but it gets the job done. Any better ideas?
Posted by Brian Langenfeld on September 21st, 2008.
Brian,
actually I implemented ignore_access_control for the test environment where I frequently need to setup objects and don’t like the access control hassle there. But you are right, there are other use cases for such a feature. I’m not sure how to separate that properly from the day-to-day business of the application, though. I’d prefer to don’t have such an option there.
As for without_access_control, I have such a method in my test helper. I plan to integrate it as test infrastructure into the plugin in the future. Looks like this in my helper:
def without_access_control
Authorization.ignore_access_control(true)
yield
ensure
Authorization.ignore_access_control(false)
end
def with_user (user)
prev_user = Authorization.current_user
Authorization.current_user = user
yield
ensure
Authorization.current_user = prev_user
end
Steffen
Posted by Steffen Bartsch on September 22nd, 2008.
Hi Steffen,
This is great. I have a question about AND-ing if_attribute or passing additional conditions. Is it possible?
These are all pseudo-code, but hopefully get the gist across.
Something like
if_attribute :owner => is{user} and if_attribute :color => is{”blue”}
Or pass named_scopes as conditions
# if_attribute :named_scope?(user) => is{true}
Or pass arguments to attributes
# if_attribute :find_this(user)
Also, could you say a bit more or give an example of the :context option in use?
Thanks,
Sarah
Posted by Sarah on November 19th, 2008.
Sarah,
> This is great. I have a question about AND-ing if_attribute or
> passing additional conditions. Is it possible?
following your example, the easiest way is to combine those attributes:
if_attribute :owner => is {user}, :color => “blue”
> Or pass named_scopes as conditions
> # if_attribute :named_scope?(user) => is{true}
>
> Or pass arguments to attributes
> # if_attribute :find_this(user)
No, currently if_attribute is limited to attributes and associations. Could you give me an extended, real-world example to convince me that this is actually necessary?
> Also, could you say a bit more or give an example of the
> :context option in use?
E.g. in
has_permission_on :employees, :to => :read
the context is :employees. In controllers, the context is usually inferred from the controller name (e.g. EmployeesController). If a different context should be employed for authorization checks, you can pass that with the :context option.
The documentation at
http://www.tzi.org/~sbartsch/declarative_authorization/0.1/
could be of additional help.
Steffen
Posted by Steffen Bartsch on November 19th, 2008.