stffn / declarative_authorization

An authorization Rails plugin using a declarative DSL for specifying authorization rules in one place

This URL has Read+Write access

stffn (author)
Mon Jul 20 04:54:45 -0700 2009
commit  3b242073fd8d8b3881b166974ac8e70c64ca0b1e
tree    369ace59a13910b42aecf101fcc8c55afec96624
parent  e13ed8a14c003b272b41bd092d7682be73d838c1
name age message
file .gitignore Tue Oct 28 12:48:23 -0700 2008 Garlic for tests with multiple versions of Rails [stffn]
file CHANGELOG Loading commit data...
file MIT-LICENSE Thu Aug 14 07:18:00 -0700 2008 initial commit [stffn]
file README.rdoc
file Rakefile Wed Feb 25 12:01:30 -0800 2009 Safer gemspec version and generator [stffn]
directory app/
file authorization_rules.dist.rb Mon Aug 18 07:03:40 -0700 2008 example authorization rules [stffn]
directory config/ Fri May 15 03:15:47 -0700 2009 GUI for change analyzer [stffn]
file declarative_authorization.gemspec
file declarative_authorization.gemspec.src
file garlic_example.rb Tue Oct 28 13:32:50 -0700 2008 Added 2.2.0-RC1 as garlic target [stffn]
file init.rb Thu Mar 05 12:16:34 -0800 2009 Restructured directories to better serve as gem... [stffn]
directory lib/
directory tasks/ Thu Aug 14 07:18:00 -0700 2008 initial commit [stffn]
directory test/
README.rdoc

Declarative Authorization

The declarative authorization plugin offers an authorization mechanism inspired by RBAC. The most notable distinction to existing authorization plugins is the declarative authorization approach. That is, authorization rules are not programmatically in between business logic but in an authorization configuration.

Currently, Rails authorization plugins only provide for programmatic authorization rules. That is, the developer needs to specify which roles are allowed to access a specific controller action or a part of a view, which is not DRY. With a growing application code base and functions, as it happens especially in agile development processes, it may be decided to introduce new roles. Then, at several places of the source code the new group needs to be added, possibly leading to omissions and thus hard to test errors. Another aspect are changing authorization requirements in development or even after taking the application into production. Then, privileges of certain roles need to be easily adjusted when the original assumptions concerning access control prove unrealistic. In these situations, a declarative approach as offered by this plugin increases the development and maintenance efficiency.

Plugin features

  • Authorization at controller action level
  • Authorization helpers for Views
  • Authorization at model level
    • Authorize CRUD (Create, Read, Update, Delete) activities
    • Query rewriting to automatically only fetch authorized records
  • DSL for specifying Authorization rules in an authorization configuration

Requirements

  • An authentication mechanism
    • User object in Controller#current_user
    • (For model security) Setting Authorization.current_user
  • User objects need to respond to a method :role_symbols that returns an array of role symbols

See below for installation instructions.

Authorization Data Model

 ----- App domain ----|-------- Authorization conf ---------|------- App domain ------

                       includes                   includes
                        .--.                        .---.
                        |  v                        |   v
  .------.  can_play  .------.  has_permission  .------------.  requires  .----------.
  | User |----------->| Role |----------------->| Permission |<-----------| Activity |
  '------' *        * '------' *              * '------------' 1        * '----------'
                                                      |
                                              .-------+------.
                                           1 /        | 1     \ *
                                 .-----------.   .---------.  .-----------.
                                 | Privilege |   | Context |  | Attribute |
                                 '-----------'   '---------'  '-----------'

In the application domain, each User may be assigned to Roles that should define the users’ job in the application, such as Administrator. On the right-hand side of this diagram, application developers specify which Permissions are necessary for users to perform activities, such as calling a controller action, viewing parts of a View or acting on records in the database. Note that Permissions consist of an Privilege that is to be performed, such as read, and a Context in that the Operation takes place, such as companies.

In the authorization configuration, Permissions are assigned to Roles and Role and Permission hierarchies are defined. Attributes may be employed to allow authorization according to dynamic information about the context and the current user, e.g. "only allow access on employees that belong to the current user’s branch."

Examples

A fully functional example application can be found at github.com/stffn/decl_auth_demo_app

Details on the demonstrated methods can be found in the API docs, either generated yourself or at www.tzi.org/~sbartsch/declarative_authorization

Controller

If authentication is in place, enabling user-specific access control may be as simple as one call to filter_access_to :all which simply requires the according privileges for present actions. E.g. the privilege index_users is required for action index. This works as a first default configuration for RESTful controllers, with these privileges easily handled in the authorization configuration, which will be described below.

    class EmployeesController < ApplicationController
      filter_access_to :all
      def index
        ...
      end
      ...
    end

When custom actions are added to such a controller, it helps to define more clearly which privileges are the respective requirements. That is when the filter_access_to call may become more verbose:

    class EmployeesController < ApplicationController
      filter_access_to :all
      # this one would be included in :all, but :read seems to be
      # a more suitable privilege than :auto_complete_for_user_name
      filter_access_to :auto_complete_for_employee_name, :require => :read
      def auto_complete_for_employee_name
        ...
      end
      ...
    end

For some actions it might be necessary to check certain attributes of the object the action is to be acting on. Then, the object needs to be loaded before the action’s access control is evaluated. On the other hand, some actions might prefer the authorization to ignore specific attribute checks as the object is unknown at checking time, so attribute checks and thus automatic loading of objects needs to be enabled explicitly.

    class EmployeesController < ApplicationController
      filter_access_to :update, :attribute_check => true
      def update
        # @employee is already loaded from param[:id] because of :attribute_check
      end
    end

You can provide the needed object through before_filters. This way, you have full control over the object that the conditions are checked against. Just make sure, your before_filters occur before any of the filter_access_to calls.

    class EmployeesController < ApplicationController
      before_filter :new_employee_from_params, :only => :create
      before_filter :new_employee, :only => [:index, :new]
      filter_access_to :all, :attribute_check => true

      def create
        @employee.save!
      end

      protected
      def new_employee_from_params
        @employee = Employee.new(params[:employee])
      end
    end

If the access is denied, a permission_denied method is called on the current_controller, if defined, and the issue is logged. For further customization of the filters and object loading, have a look at the complete API documentation of filter_access_to in Authorization::AuthorizationInController::ClassMethods.

Views

In views, a simple permitted_to? helper makes showing blocks according to the current user’s privileges easy:

    <% permitted_to? :create, :employees do %>
    <%= link_to 'New', new_employee_path %>
    <% end %>

Only giving a symbol :employees as context prevents any checks of attributes as there is no object to check against. For example, in case of nested resources a new object may come in handy:

    <% permitted_to? :create, Branch.new(:company => @company) do
            # or @company.branches.new
            # or even @company.branches %>
    <%= link_to 'New', new_company_branch_path(@company) %>
    <% end %>

Lists are straight-forward:

    <% for employee in @employees %>
    <%= link_to 'Edit', edit_employee_path(employee) if permitted_to? :update, employee %>
    <% end %>

See also Authorization::AuthorizationHelper.

Models

There are two destinct features for model security built into this plugin: authorizing CRUD operations on objects as well as query rewriting to limit results according to certain privileges.

See also Authorization::AuthorizationInModel.

Model security for CRUD opterations

To activate model security, all it takes is an explicit enabling for each model that model security should be enforced on, i.e.

    class Employee < ActiveRecord::Base
      using_access_control
      ...
    end

Thus,

    Employee.create(...)

fails, if the current user is not allowed to :create :employees according to the authorization rules. For the application to find out about what happened if an operation is denied, the filters throw Authorization::NotAuthorized exceptions.

As access control on read are costly, with possibly lots of objects being loaded at a time in one query, checks on read need to be actived explicitly by adding the :include_read option.

Query rewriting using named scopes

When retrieving large sets of records from databases, any authorization needs to be integrated into the query in order to prevent inefficient filtering afterwards and to use LIMIT and OFFSET in SQL statements. To keep authorization rules out of the source code, this plugin offers query rewriting mechanisms through named scopes. Thus,

    Employee.with_permissions_to(:read)

returns all employee records that the current user is authorized to read. In addition, just like normal named scopes, query rewriting may be chained with the usual find method:

    Employee.with_permissions_to(:read).find(:all, :conditions => ...)

If the current user is completely missing the permissions, an Authorization::NotAuthorized exception is raised. Through Model.obligation_conditions, application developers may retrieve the conditions for manual rewrites.

Authorization Rules

Authorization rules are defined in config/authorization_rules.rb. E.g.

    authorization do
      role :admin do
        has_permission_on :employees, :to => [:create, :read, :update, :delete]
      end
    end

There is a default role :guest that is used if a request is not associated with any user or with a user without any roles. So, if your application has public pages, :guest can be used to allow access for users that are not logged in. All other roles are application defined and need to be associated with users by the application.

Privileges, such as :create, may be put into hierarchies to simplify maintenance. So the example above has the same meaning as

    authorization do
      role :admin do
        has_permission_on :employees, :to => :manage
      end
    end

    privileges do
      privilege :manage do
        includes :create, :read, :update, :delete
      end
    end

Privilege hierarchies may be context-specific, e.g. applicable to :employees.

    privileges do
      privilege :manage, :employees, :includes => :increase_salary
    end

For more complex use cases, authorizations need to be based on attributes. E.g. if a branch admin should manage only employees of his branch (see Authorization::Reader in the API docs for a full list of available operators):

    authorization do
      role :branch_admin do
        has_permission_on :employees do
          to :manage
          # user refers to the current_user when evaluating
          if_attribute :branch => is {user.branch}
        end
      end
    end

To reduce redundancy in has_permission_on blocks, a rule may depend on permissions on associated objects:

    authorization do
      role :branch_admin do
        has_permission_on :branches, :to => :manage do
          if_attribute :managers => contains {user}
        end

        has_permission_on :employees, :to => :manage do
          if_permitted_to :manage, :branch
          # instead of
          #if_attribute :branch => {:managers => contains {user}}
        end
      end
    end

Lastly, not only privileges may be organized in a hierarchy but roles as well. Here, project manager inherit the permissions of employees.

      role :project_manager do
        includes :employee
      end

See also Authorization::Reader.

Testing

declarative_authorization provides a few helpers to ease the testing with authorization in mind.

In your test_helper.rb, to enable the helpers add

    require File.expand_path(File.dirname(__FILE__) +
        "/../vendor/plugins/declarative_authorization/lib/maintenance")

    class Test::Unit::TestCase
      include Authorization::TestHelper
      ...
    end

Now, in unit tests, you may deactivate authorization if needed e.g. for test setup and assume certain identities for tests:

    class EmployeeTest < ActiveSupport::TestCase
      def test_should_read
        without_access_control do
          Employee.create(...)
        end
        assert_nothing_raised do
          with_user(admin) do
            Employee.find(:first)
          end
        end
      end
    end

In functional tests, get, posts, etc. may be tested in the name of certain users:

    get_with admin, :index
    post_with admin, :update, :employee => {...}

See Authorization::TestHelper for more information.

Installation of declarative_authorization

One of three options to install the plugin:

  • Install by Gem: Add to your environment.rb in the initializer block:
      config.gem "stffn-declarative_authorization", :lib => "declarative_authorization"
    

    And call from your application’s root directory

      rake gems:install
    
  • Alternatively, to install from github, execute in your application’s root directory
      cd vendor/plugins && git clone git://github.com/stffn/declarative_authorization.git
    
  • Or, download one of the released versions from Github at github.com/stffn/declarative_authorization/downloads

Then,

  • provide the requirements as noted below,
  • create a basic config/authorization_rules.rb—you might want to take the provided example authorization_rules.dist.rb in the plugin root as a starting point,
  • add filter_access_to, permitted_to? and model security as needed.

Providing the Plugin’s Requirements

The requirements are

  • Rails >= 2.1 and Ruby >= 1.8.6, including 1.9
  • An authentication mechanism
  • A user object returned by Controller#current_user
  • An array of role symbols returned by User#role_symbols
  • (For model security) Setting Authorization.current_user to the request’s user

Of the various ways to provide these requirements, here is one way employing restful_authentication.

  • Install restful_authentication
     cd vendor/plugins && git clone git://github.com/technoweenie/restful-authentication.git restful_authentication
     cd ../.. && ruby script/generate authenticated user sessions
    
  • Move "include AuthenticatedSystem" to ApplicationController
  • Add filter_access_to calls as described above.
  • If you’d like to use model security, add a before_filter that sets the user globally to your ApplicationController. This is thread-safe.
     before_filter :set_current_user
     protected
     def set_current_user
       Authorization.current_user = current_user
     end
    
  • Add roles field to the User model through a :has_many association (this is just one possible approach; you could just as easily use :has_many :through or a serialized roles array):
    • create a migration for table roles
       class CreateRoles < ActiveRecord::Migration
         def self.up
           create_table "roles" do |t|
             t.column :title, :string
             t.references :user
           end
         end
      
         def self.down
           drop_table "roles"
         end
       end
      
    • create a model Role,
       class Role < ActiveRecord::Base
         belongs_to :user
       end
      
    • add has_many :roles to the User model and a roles method that returns the roles as an Array of Symbols, e.g.
       class User < ActiveRecord::Base
         has_many :roles
         def role_symbols
           (roles || []).map {|r| r.title.to_sym}
         end
       end
      
    • add roles to your User objects using e.g.
       user.roles.create(:title => "admin")
      

Note: If you choose to generate an Account model for restful_authentication instead of a User model as described above, you have to customize the examples and create a ApplicationController#current_user method.

Debugging Authorization

Currently, the main means of debugging authorization decisions is logging and exceptions. Denied access to actions is logged to warn or info, including some hints about what went wrong.

All bang methods throw exceptions which may be used to retrieve more information about a denied access than a Boolean value.

Authorization Browser

If your authorization rules become more complex, you might be glad to use the authorization rules browser that comes with declarative_authorization. It has a syntax-highlighted and a graphical view with filtering of the current authorization rules.

By default, it will only be available in development mode. To use it, add the following lines to your authorization_rules.rb for the appropriate role:

  has_permission_on :authorization_rules, :to => :read

Then, point your browser to

  http://localhost/authorization_rules

The browser needs Rails 2.3 (for Engine support). The graphical view requires Graphviz (which e.g. can be installed through the graphviz package under Debian and Ubuntu) and has only been tested under Linux.

Help and Contact

We have an issue tracker for bugs and feature requests as well as a Google Group for discussions on the usage of the plugin. You are very welcome to contribute. Just fork the git repository and create a new issue, send a pull request or contact me personally.

Maintained by

Steffen Bartsch TZI, Universität Bremen, Germany sbartsch at tzi.org

Contributors

Thanks to

  • Eike Carls
  • Erik Dahlstrand
  • Jeremy Friesen
  • Brian Langenfeld
  • Geoff Longman
  • Olly Lylo
  • Mark Mansour
  • Mike Vincent

Licence

Copyright © 2008 Steffen Bartsch, TZI, Universität Bremen, Germany released under the MIT license