Role based access (RBAC) for Rails

Tagged
rails ruby datamapper

The basics of a role based access control system using Ruby on Rails, CanCan and Devise.

Goals

This are often the requirements/desires I have with an authentication/authorisation system:

RBAC authorisation is reasonably common in larger projects and is well described in the Wikipedia article

We would like users to be assigned roles and roles enabling a number of permissions. A user may have a number of different roles meaning the permissions available for each of their roles is combined.

So we will be able to access a user’s permissions (using DataMapper) like so:

#!ruby
u = User.get(1)
u.roles.permissions.each {|perm|
  #...
}

Models

First of all, get devise and cancan installed and basically configured within your application. I won’t cover this here.

These are the models that we will eventually have and their corresponding relationships. These examples use DataMapper.

#!ruby
class User
  include DataMapper::Resource

  # devise setup
  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable


  property :id, Serial
  property :email, String, :required => true, :unique => true, :format => :email_address
  property :active, Boolean
  # ...

  has n, :roles, :through => Resource
  # ...

  # for assignment of roles
  def role_ids=(ids)
    self.roles.clear
    ids.delete_if{|i| i.empty?}.each do |id|
      self.roles << Role.get(id)
    end
  end

  def has_role?(role_sym)
    roles.any? { |r| r.name.underscore.to_sym == role_sym }
  end

  def role?(role)
    return !!self.roles.first(:name => role.to_s.camelize)
  end
end

class Role
  include DataMapper::Resource

  property :id, Serial
  property :name, String
  property :description, Text
  # ...

  has n, :users, :through => Resource
  has n, :permissions, :through => Resource
end

We are using the anonymous join model Resource to link users and roles. We could quite easily make an actual model to hold extra information (for a history, perhaps).

There are a few extra methods in the User model to help with the assignment of roles. This is DataMapper’s way of overriding accessors and enables specific implementations.

CanCan setup

The permissions for many applications are closely tied to the application’s code. The checks are hard coded into the application. If this is the case there is no need to have the permissions taken from the database. The roles, yes, but the defining permissions will only ever change with the application code.

We can then put the permissions in a static file (YAML) in this case and have it read by the app when needed. CanCan’s ‘ability’ model is a good place to do this:

#!ruby
class Ability
  include CanCan::Ability

  @@permissions = nil

  def initialize(user)
    self.clear_aliased_actions

    alias_action :index, :show, :to => :read
    alias_action :new,          :to => :create
    alias_action :edit,         :to => :update
    alias_action :destroy,      :to => :delete

    user ||= User.new

    # super user can do everything
    if user.role? :super
      can :manage, :all
    else
      # edit update self
      can :read, User do |resource|
        resource == user
      end
      can :update, User do |resource|
        resource == user
      end
      # enables signup
      can :create, User

      user.roles.each do |role|
        if role.permissions
          role.permissions.each do |perm_name|
            unless Ability.permissions[perm_name].nil?
              can(Ability.permissions[perm_name]['action'].to_sym, Ability.permissions[perm_name]['subject_class'].constantize) do |subject|
                Ability.permissions[perm_name]['subject_id'].nil? ||
                  Ability.permissions[perm_name]['subject_id'] == subject.id
              end
            end
          end
        end
      end
    end
  end

  def self.permissions
    @@permissions ||= Ability.load_permissions
  end

  def self.load_permissions(file='permissions.yml')
    YAML.load_file("#{::Rails.root.to_s}/config/#{file}")
  end
end

This logic is up to you and your project but the above basic implementation does the following:

You can then start using the action, subject and optional object paramaters with CanCan to check for permissions for which the CanCan documentation has many examples.

Initial Setup

We can then define a few roles to seed the database with for use later. Put this in db/seed.rb:

#!ruby
super_user = User.create(:email => 'super@example.com',
                         :firstname => 'Super',
                         :surname => 'User',
                         :password => 'password',
                         :password_confirmation => 'password',
                         :active => true,
                         :date => Time.now)
admin_user = User.create(:email => 'admin@example.com',
                         :firstname => 'Admin',
                         :surname => 'User',
                         :password => 'password',
                         :password_confirmation => 'password',
                         :active => true,
                         :date => Time.now)
user = User.create(:email => 'user@example.com',
                   :firstname => 'User',
                   :surname => 'User',
                   :password => 'password',
                   :password_confirmation => 'password',
                   :active => true,
                   :date => Time.now)

# create roles
super_role = Role.create(:name => 'super', :description => 'Super user')
admin_role = Role.create(:name => 'admin', :description => 'Admin user')
user_role  = Role.create(:name => 'user', :description => 'Normal user')

# get our permissions
permissions = YAML.load_file("#{::Rails.root.to_s}/config/permissions.yml")

# assign permissions
admin_role.permissions = permissions.collect{|n,p| n}
admin_role.save

# assign roles
super_user.roles << super_role
super_user.save
admin_user.roles << admin_role
admin_user.save
user.roles << user_role
user.save

The actual permissions seeded will be entirely up to you and your application but this sets up a few basics for one model.

Further

This also enables you to have a nice report of the roles and their permissions much like the following: RBAC report screenshot

This can be created in the view something like this:

#!ruby
= form_tag({:controller => 'roles', :action => 'report'}, :method => 'post') do
  %table
    %tr
      %th= Role.human_attribute_name('permissions')
      - @roles.sort.each do |role|
        %th
          = role.name
          = hidden_field_tag "permissions[#{role.name}][]", ""
    - Ability.permissions.each do |pname, pdetails|
      %tr
        %td= pdetails['description']
        - @roles.sort.each do |role|
          %td
            = check_box_tag "permissions[#{role.name}][]", pname, role.permissions.include?(pname)
  %fieldset.actions
    = submit_tag 'Save'

Dynamic Permissions

It is quite possible that you may want to have dynamic permissions. For example, to assign permissions to specific object instances or perhaps in an application that accesses another service which changes.

If this is the case, you will need to have the permissions based in a dynamic datastore, most likely the same database. You would need to define a new model to store them and update the associations and CanCan logic to do authorisation checks.

[RBAC]: Role Based Access Control