Declarative Authorization

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.

42 comments.

  1. [...] 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. [...]

  2. 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

  3. 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

  4. 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

  5. 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

  6. 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

  7. 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

  8. 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

  9. 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

  10. 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!

  11. 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!

  12. 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

  13. 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!

  14. 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

  15. 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

  16. > 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

  17. 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

  18. 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?

  19. 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

  20. 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

  21. 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

  22. 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.

  23. 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

  24. 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…

  25. 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

  26. 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

  27. 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

  28. 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.

  29. 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

  30. 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

  31. 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

  32. Thanks, Mike. Much better suited than this comments thread :)

  33. 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.. :>

  34. 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

  35. 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!! :>

  36. 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

  37. 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.

  38. 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

  39. [...] 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 [...]

  40. [...] Rails apps, our Rails authorization plugin declarative_authorization comes with  support of this kind. In the screenshot, controller authorization analysis is shown. [...]

  41. [...] control list solution for my Ruby needs I came across Steffen Bartsch’s “Declarative Authorization” plugin (Github source [...]

  42. [...] 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 [...]