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.
Hi Steffen,
More specifically my question is this:
In my application, a user can belong to many projects.
Within the context of each project, each user has only one role.
However, across the site, the user can have as many different roles as projects.
So I need to know, not only does a user have an admin role, but does a user have an admin role for a particular project.
In essence, can I use this plugin to find out “What is the user’s role on a particular project”
Thanks.
Posted by Sarah on November 23rd, 2008.
Sarah,
good point. Currently, there is no concept of dynamic roles depending on the context, such as the project. Instead, you can model such a case through authorization constraints. E.g. if your project model looks like this
class Project …
has_many :project_users
has_many :users, :through => :project_users
has_many :owners, :through => :project_users, :conditions => “project_users.role = ‘owner’”
has_many :members, :through => :project_users, :conditions => “project_users.role = ‘member’”
…
end
You then can use the associations :owners, :members etc. to authorize your users, e.g.
role :user
has_permissions_on :projects, :to => [:read, :update, :delete] do
if_attribute :owners => contains {user}
end
has_permissions_on :projects, :to => :read do
if_attribute :members => contains {user}
end
end
Still, I would be very interested to include a concept of dynamic roles as this seems to be quite a common case. Any ideas how this should look like? What do you think about:
role :project_owner, :in_context => :projects
if_attribute :owners => contains {user}
has_permissions_on :projects, to => [:read, :update, :delete]
end
role :project_member, :in_context => :projects
if_attribute :members => contains {user}
has_permissions_on :projects, to => :read
end
Steffen
Posted by Steffen Bartsch on November 24th, 2008.
What about having something like “if_not_attribute” to exclude attribute values from the scope?
Example:
if_not_attribute :project_kind => ‘top_secret’
One more thing: Using arrays would be nice.
Example:
if_attribute :project_status => ['active', 'pending']
Last thing :)
Why don’t you use Lighthouse (www.lighthouseapp.com) for tickets? It is much better than adding comments to this blog post…
Posted by Georg Ledermann on November 28th, 2008.
Hi Steffen,
thank you for giving this very powerful plugin .
But is there anyway to use these operators?(not,>,>=…) and how can I change my code to use the plugin with following conditions.
All the attribute conditons is and’ed not OR’ed.
Thanks
has_permission_on :projects do
to :bid
# if_attribute :level => not is {3}
# if_attribute :managers => not contains {user}
# if_attribute :price => < {500}
end
Posted by cquaker on November 28th, 2008.
Georg,
I thought about creating a lighthouse project before. Now it really materialized. See
http://stffn.lighthouseapp.com/projects/20733-declarative_authorization/overview
I moved both suggestions to lighthouse tickets.
Steffen
Posted by Steffen Bartsch on November 28th, 2008.
Hi cquaker,
I moved your request to
http://stffn.lighthouseapp.com/projects/20733-declarative_authorization/tickets/3-additional-if_attribute-operators
Lets move over there for further discussion.
Steffen
Posted by Steffen Bartsch on November 28th, 2008.
Hi,
I’m trying to work out a read permission on a model – but I’m getting a strange SQL error that I haven’t been able to resolve.
I need to limit access to objects where the user is not associated. Probably just some code will explain it best…
class Build “User”,
:finder_sql => “select users.* from users inner join customer_groups on customer_groups.user_id = users.id where customer_groups.customer_id = #{:customer_id}”
using_access_control :include_read => true
end
# from authorization_rules.rb
#
role :customer_account do
has_permission_on :builds do
to :read
if_attribute :accessors => contains{user}
end
end
In an rspec model test, I have…
before(:each) do
@current_user = users(:hp_cpc_customer)
Authorization.stub!(:current_user).and_return(@current_user)
end
it “should …” do
Build.with_permissions_to(:read).size.should == customers(:hp_cpc).builds.size
end
And I keep getting this error…
LINE 1: …S count_all FROM “builds” INNER JOIN “users” ON users.buil…
^
: SELECT count(*) AS count_all FROM “builds” INNER JOIN “users” ON users.build_id = builds.id WHERE ((“users”.id = 1046015453))
It’s generating this bad SQL, trying to join Build to User but it should be using the :finder_sql defined for the has_many :accessors in the Build class.
I’m not sure if I’m doing something wrong here or this is a DA issue.
Any help very much appreciated, thanks.
Posted by andy on December 6th, 2008.
Hi andy,
I suppose some code of the model class was lost in the comment. Consider posting it on the Lighthouse project.
http://stffn.lighthouseapp.com/projects/20733-declarative_authorization/overview
I’m not sure, how d_a plays along with finder_sql, though. It relies on Rails’ named_scope features to implement the query rewriting. If that doesn’t work on finder_sql, some manual SQL might be necessary.
Steffen
Posted by Steffen Bartsch on December 6th, 2008.
Hi Steffen,
I really like the syntax you suggested for the context cases:
role :project_owner, :in_context => :projects
if_attribute :owners => contains {user}
has_permissions_on :projects, to => [:read, :update, :delete]
end
The pseudo-code I wanted to work, which means you’d have to be able to query the project in the scope of this has_permission block, was this:
role :member do
has_permission_on :project_elements do
to :update; if_attribute :created_by => is {user} and project.members.include?(user)
end
end
I ended up doing a ‘roll your own’ authorization to cover those cases, and based the view syntax on yours because it’s so clean; but my implementation is a more a collection of app-specific rules rather than something that could be extended / expanded.
Adding these cases to declarative_authorization would be great. I think it is great as it is, and this would make it that much more extensible.
Thanks for getting back to me.
Sarah
Posted by Sarah on December 8th, 2008.
Hey Steffen, all..
We are preparing to use this authorization system on a couple projects, looks very awesome. I was hoping for a broader medium for discussing how people were using it, etc.. so I started a google group:
http://groups.google.com/group/declarative_authorization
Posted by Mike on December 11th, 2008.
Thanks, Mike. Much better suited than this comments thread :)
Posted by Steffen Bartsch on December 11th, 2008.
great plugin.. been trying to get has_role? working in a helper that builds my menu.. submenu.roles returns [guest,admin] current_user.roles returns the same. but a call to has_role?(submenu.roles) ….. always returns zero.. and ideas? I can email my code if needed.. tia.. :>
Posted by steven p on December 21st, 2008.
has_role? accepts multiple role symbols, but only as multiple arguments, e.g. has_role?(:guest, :admin). To make your case work, use has_role?(*submenu.roles) to expand the array to multiple arguments.
has_role? is not meant to provide authorization, though. permitted_to? with appropriate permissions is by far more flexible. Just for consideration.
Steffen
Posted by Steffen Bartsch on December 21st, 2008.
Using authorization with active scaffold. How can you tell what actions are needed in the authorization_rules.rb tried adding
:nested, :show_search, :row, :update_column, :destroy_existing, :edit_associated, :update_table
to manage. But I’m still missing something!! :>
Posted by steven on January 8th, 2009.
I haven’t worked with active scaffold, yet. If your request is denied, the log should tell you the action that was called.
Please start a threat at the Google Group
http://groups.google.com/group/declarative_authorization
Steffen
Posted by Steffen Bartsch on January 9th, 2009.
Hey Steffen,
What a great plugin! Thanks a million for this, it will make all the difference to future projects and to my coding style in general.
I was wondering if it were possible to specify namespaces in authorization_rules.rb? I have an Admin::UsersController < UsersController, but i don’t know how to specify permissions on it.
Thanks again,
Matt.
Posted by Matt on January 18th, 2009.
Hi Matt,
currently, namespaces are not explicitly supported by decl_auth. Until a proper automatic way is established, you could just specify a custom context for filter_access_to, e.g.
filter_access_to :all, :context => :admin_users
and define permissions for that context.
For a wider audience, please use the Google Group to further discuss these kind of issues.
http://groups.google.com/group/declarative_authorization
Steffen
Posted by Steffen Bartsch on January 19th, 2009.
[...] have the authorization rules in your Rails app defined in a clear DSL, such as the one offered by declarative_authorization. Still, with anything more than a few roles and models (let’s not even think about 200 [...]
Posted by steffenbartsch — Graphically Browse Your Authorization Rules on February 17th, 2009.
[...] Rails apps, our Rails authorization plugin declarative_authorization comes with support of this kind. In the screenshot, controller authorization analysis is shown. [...]
Posted by steffenbartsch — Using Your Authorization Framework Correctly? on March 8th, 2009.
[...] control list solution for my Ruby needs I came across Steffen Bartsch’s “Declarative Authorization” plugin (Github source [...]
Posted by Using Declarative Association with HABTM Relationships- I’m a Richardson, RU? on August 3rd, 2009.
[...] clearance and authlogic. Seemingly there are a lot authentication gems and engines out there e.g. Declarative Authentication or ACL9 just to name a few. But there is tons of other good stuff goin’ on: Pacecar is an [...]
Posted by Rails Camp Germany 3 – Saturday sum up « IO 9elements on November 29th, 2009.