Table of Contents

1 Introduction

ActiveRbac is a Ruby On Rails plugin that supports you in implementing an Role Based Access Control (RBAC) system for your application.

It does so by defining a standard schema (FIXME: link) and providing you with mixins to

1.1 Spelling

You might see multiple different spellings for ActiveRbac. The author of the plugin likes the following:

The author formerly also referenced it as ActiveRBAC since RBAC is the abbreviation for Role Based Access Control but is not using it any more.

2 The ActiveRBAC Schema

We will define two schemas for objects supporting an Role Based Access Control (RBAC) system:

2.1 RBAC schema level 1

Users and roles are required in any RBAC schema: In an RBAC system, users represent humans or computers which can access objects. Permissions to access objects are granted to roles which are assigned to users.

The following image shows the methods, your user and role objects must provide. In classic Java speech: Your user and role classes have to implement the interfaces to support RBAC schema level 1.

ActiveRBAC schema level 1

Note that this behaviour only describes methods that are used ot read attributes. This means you can store your users and roles in any way and extend them however you want as long as you provide the methods in the interface.

2.1.1 Role Interface

Your role class must provide the the method identifier which returns a string identifying this role. The string should match the regular expression /^[a-z.]+$/ and look like “roles.admin”, “roles.user” or “roles.anonymous”.

2.1.2 User Interface

Your user class must provide the method has_role?, roles.

2.2 RBAC schema level 2

If you have several roles and do not want to hardcode the role names in your permission checks as follows then the StaticPermission interface might come in handy.

if current_user.has_role?('roles.admin', 'roles.user', 'roles.foo') then
  # ...
else
  raise "You are not allowed to do this!"
end

RBAC schema level 2 extends level 1 by a StaticPermission class and adds some methods to the User and Role class. Basically, StaticPermissions are strings like “permissions.reboot_machine” that can be granted to roles.

A user is granted the role through his roles he has been assigned.

The schema is shown in the image below.

ActiveRBAC schema level 2

2.2.1 Role Interface

The Role interface is extended by the following methods:

2.2.2 User Interface

The User interface is extended by the following methods:

2.2.3 StaticPermission Interface

Your static permission class must provide the the method identifier which returns a string identifying this permission. The string should match the regular expression /^[a-z.]+$/ and look like “permission.create_article, “permissions.stop_server or “permission.foo”.

2.3 Sample Implementation

The code below shows a simple sample implementation of static user, role and static permission classes for RBAC schema level 2.

class User
  attr_reader :roles
  alias_method :all_roles, :roles

  attr_reader :login

  def initialize(login, roles)
    @login = login
    @roles = role.dup
  end
end

class Role
  attr_reader :identifier
  attr_reader :permissions

  def initialize(identifier, permissions)
    @identifier = identifier.dup
    @permissions = permissions.dup
  end

  def has_static_permission?(identifier)
    @permissions.any? { |perm| perm.identifier == identifier }
  end
end

class StaticPermission
  attr_reader :identifier

  def initialize(identifier)
    @identifier = identifier.dup
  end
end

Now, we could do something like this:

>> permissions = Array.new
>> permissions << StaticPermission.new("permissions.create_article")
>> permissions << StaticPermission.new("permissions.shutdown_server")
>>
>> role = Role.new("roles.admin", permission)
>>
>> user = User.new("root", [ role ])
>>
>> user.has_role?("roles.admin")
=> true
>> user.has_role?("roles.anonymous")
=> false
>> user.has_static_permission?("permissions.create_article")
=> true
>> user.has_static_permission?("permissions.rm -rf /")
=> false

3 acts as current user container

acts_as_current_user_container is a mixin for ActionController::Base classes.

3.1 Gotcha

Note that you have to call current_user= (the setter) with self.current_user=. Otherwise, the local variable current_user will be set. We we will write self.current_user below to stress this. Note that with self.current_user we mean the object method not a class method!

3.2 Motivation

All Rails applications featuring access control must store the currently logged in user in the session. Sessions are only available from your controllers so you will either have a lot of session[:current_user_id] calls in your code or write a method to access the current user via this method.

This method is very simple and thus it can perfectly be refactored into a mixin for your controllers. We have done just that and wrote acts_as_current_user_container.

3.3 First Glance

Let have a quick look on how it works before diving into detail (you could also call this section “for the impatient”). If you want to access the current user in multiple locations then you will want to put the call to acts_as_current_user_container into your ApplicationController:

class ApplicationController < ActionController::Base
  acts_as_current_user_container
end

Afterwards, you can get access to the current user in your controllers by the methods current_user and self.current_user. For example:

class SecretController < ActionController::Base
  before_filter :protect_me

  protected

    def protect_me
      if current_user.nil? then
        session[:errors] = 'Keep out!'
        redirect_to login_url()
        return false
      end

      true
    end
end

As you would guess by the example above, the current_user returns nil if no user has previously been set. This makes for some checks whether the result is nil or is not nil. You can circumvent this problem by passing in a symbol specifying a class (or the class itself):

class AnonymousUser
  acts_as_anonymous_user
end

class ApplicationController < ActionController::Base
  acts_as_current_user_container :anonymous_user => :anonymous_user
end

Then, current_user creates an instance of AnonymousUser and returns it if no user has been set explicitely. See the documentation on acts_as_anonymous_user for more information about this mixin.

3.4 Requirements

The acts_as_current_user_container mixin works with any ActionController::Base class (in fact the only dependency is the existence of a session method returning a Hash).

You must not use the session variable :aacuc_data, i.e. session[:aacuc_data] should not be accessed from your code.

3.5 Reference

When you use the mixin in your ActionController::Base class then the two methods current_user and self.current_user are added to this controller class. You can set an object that represents the current user by using the second helper method. The first one returns the object earlier set.

If you call current_user without calling self.current_user before in the current session then the result depends on the parameters you passed to the acts_as_current_user_container method. At the moment, this method accepts but one parameter - :anonymous_user. The following are valid values for :anonymous_user

Examples:

class ApplicationController < ActionController::Base
  acts_as_current_user_container :anonymous_user => nil
end

class ApplicationController < ActionController::Base
  acts_as_current_user_container :anonymous_user => :anonymous_user
  # Results in AnonymousUser.new to be returned by current_user
  # unless something has explicitely set within this session.
end

class ApplicationController < ActionController::Base
  acts_as_current_user_container :anonymous_user => AnonymousUser
  # Results in AnonymousUser.new to be returned by current_user
  # unless something has explicitely set within this session.
end

If the value of current_user is an ActiveRecord::Base object then the id and the class will be stored in the session. If the value of current_user implements the informal PersistableInSession interface (see below) then the methods of the interface will be used to store the class and the data necessary for unpersisting in the session.

In all other cases only the class will be stored in the session and {previous current_user}.class.new will be returned on the next call to current_user. No state is preserved in this case.

3.6 The Informal Interface PersistableInSession

If an object that is to be written into the session data via current_user is not an ActiveRecord::Base object and needs to preserve its state over requests in the session then the object and its class must provide the following methods. We refer to them as the “informal PersistableInSession interface”. In a language like Java they would be collected in an interface but since Ruby does not have (and only arguably needs) interfaces it is only informal:

The information to be stored in the session should be as few as possible: For example, a numeric user id, a login name or a pair of the login name and the Windows Domain.

If your user data is comes from an LDAP server then your User class could look like the following:

File app/model/user.rb:

class User
  attr_accessor :login

  def persist
    { :login => self.login }
  end

  def self.unpersist(hash)
    user = User.new
    user.login = hash[:login]

    return user
  end
end

3.7 Security Implications

You should be careful only to persist the absolutely minimal information that is required to identify a user into the session. The name of the class of the object to unpersists and the object id for ActiveRecord objects is a good example of this.

If you store a password hash then this information will be written into the session file - if you use file based sessons - in a temporary directory.

Storing information like permissions or information about related ActiveRecord::Base objects could lead to incoherent data: The user object you store in the session has permissions and roles that have been revoked from the given user from the database.

Other problem scenarious could be inexplicable crashes because your user object tries to reference no longer existing articles.

3.8 Testing Gotcha

Note that calling @controller.current_user will not work in your tests before you called either post or get. You can set the current user in @controller in tests by using the Test Helpers described below.

3.9 Test Helpers

ActiveRbac comes with a helper for your controller tests. You can use it by including active_rbac/test_helper in your tests.

The helper adds the method set_current_user to Test::Unit::Test. This method works exactly as the current_user= method in your acts_as_current_user controllers.

3.9.1 Example

File app/controllers/application.rb:

  class ApplicationController < ActionController::Base
    acts_as_current_user_container :anonymous_user => AnonymousUser
  end

File app/controller/test_controller.rb:

  class TestController < ApplicationController
    def action_that_requires_login
      render :text => current_user.inspect
    end
  end

File app/test/functional/test_helper.rb:

# ...
require 'active_rbac/test_helper'
# ...

File app/test/functional/test_controller_test.rb:

# ...
def test_action_that_requires_login
  set_current_user(users(:first))

  get :action_that_requires_login
end
# ...

4 act as encrypts password

act_as_encrypts_password is a ActiveRecord::Base mixin that allows automatic encryption of passwords.

4.1 Motivation

When implementing a user model then a mandatory requirement is to encrypt the user’s password. This functionality is orthogonal to all other parts of the application: There is no interdependency between the encryption functionality of your user model and any other part of your system.

ActiveRBAC provides the mixin act_as_encrypts_password for ActiveRecord::Base. This mixin implements automatic and transparent password encryption: Whenever you set the password and store the record, the generated methods salt the password and encrypt the salted password with a cryptographic hash function. The salt is randomly generated to provide protection against Rainbow Table attacks.

4.2 First Glance

Let’s have a really quick glance over how to use act_as_encrypts_password. Create a new database table users and a User model and drop in the mixin:

class User < ActiveRecord::Base
  acts_as_encrypts_password
end

And you have password encryption on the fly.

4.3 Requirements

The encrypted password mixin can only be used with ActiveRecord::Base classes. The database table of the ActiveRecord must have the following columns:

Note that in a later release you will be able to replace “password” by any other string like “passwd” by passing a parameter to act_as_encrypts_password.

4.4 Reference

When you mark an ActiveRecord::Base class with this mixin then nifty meta programming Ruby magic will modify the class in class in the following way:

Additionally, the following validations will be added to the ActiveRecord:

4.5 Example

File db/migrations/001create_users.rb:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      # a user needs a login, right?
      t.column :login               :string, :limit => 20,
        :null => false  

      # columns for acts_as_encrypts_password
      t.column :password,           :string, :limit => 128, # sha-512 ready
        :null => false
      t.column :password_salt,      :string, :limit => 100, 
        :null => false
      t.column :password_hash_type, :string, :limit =>  10, 
        :null => false
    end

    # ...
  end

File app/model/user.rb:

class User < ActiveRecord::Base
  acts_as_encrypts_password

  validates_presence_of :login
end

Now we could use this User class in the following way:

$> cd rbac_using_project
$> ./script/console
Loading development environment.
>> user = User.new
=> <#User...
>> user.password_new?
=> true
>> user.login = 'me'
=> "me"
>> user.password = 'password'
=> "password"
>> user.password_confirmation = 'password'
=> "password"
>> user.save!
=> true
>> user.password_new?
=> false
>> user.password_salt
=> "rzTK3WRlPK"
>> user.password
=> "b5f8673fb23319c365e5f0968c55bc17db263e55"
>> require 'digest/sha1'
=> true
>> Digest::SHA1.hexdigst('password' + 'rzTK3WRlPK')
=> "b5f8673fb23319c365e5f0968c55bc17db263e55"
>> user.password_equals?('password')
=> true
>> user.password_equals?('passwd')
=> false

4.6 TODO

5 RBAC Schema Mixins

The ActiveRecord::Base mixins acts_as_user, acts_as_role and acts_as_static_permission allow for automatically registering ActiveRecord classes to adhere to the ActiveRecord

5.1 Motivation

If you want to use a fairly simple Role Based Access Control (RBAC) system with Rails then you will implement Users, Roles and most propably something equivalent to StaticPermission - the ActiveRbac schema described in FIXME.

ActiveRbac ships with the mixins acts_as_user, acts_as_role and acts_as_static_permission which allow you to create the models for a RBAC quickly: You create the database schema and the model classes. Then, you use the mixins to mark the classes to work as ActiveRbac model classes and the relations between the classes are automatically added.

5.2 First Glance

We create the model classes User, Role and StaticPermission and improve them using our acts_as mixins:

class User < ActiveRecord::Base
  acts_as_user
end

class Role < ActiveRecord::Base
  acts_as_role
end

class StaticPermission < ActiveRecord::Base
  acts_as_static_permission
end

Now, we can do the following:

>>> Role.new({ :identifier => 'roles.admin' }).save!
>>> StaticPermission.new({ :identifier => 'permissions.all' }).save!

>>> user = User.new
>>> user.save!
>>> user.roles << Role.find_by_identifier('roles.admin')

>>> user.has_role?('roles.admin')
=> true
>>> user.has_permission?('permissions.all', 'foo')
=> true
>>> user.has_permission?('foo')
=> false

5.3 Requirements

You can either implement ActiveRbac Schema level 1 or level 2 ith the acts_as model macros provided by ActiveRBAC. For level 1, you have to provide the database tables users and roles and the join table roles_users. For level 2, you also have to provide the database table static_permissions and the join table roles_static_permissions.

The roles and the static_permission tables both have to provide the string column identifier.

In Ruby code, your migrations must create at least the columns the following migration creates (of course you do not have to create the tables for static_permissions etc. if you do not want to implement level 2).

class AddUsers < ActiveRecord::Migration
  def self.up
    say "Creating table 'users' and indexes..."

    ActiveRecord::Base.transaction do
      say "Create table users.", true
      create_table(:users) do |t|
      end

      say "Add indexes.", true
      add_index :users, :email

      say "Create table roles.", true
      create_table(:roles) do |t|
        t.column :identifier,         :string,    :limit => 100, :null => false
      end

      say "Add indexes on table 'roles'.", true
      add_index :roles, :identifier

      say "Add relation table roles <---> users", true
      create_table(:roles_users, :id => false) do |t|
        t.column :role_id,            :integer, :null => false
        t.column :user_id,            :integer, :null => false
      end

      say "Add indexes on 'roles_users'", true
      add_index :roles_users, [ :role_id, :user_id ], :unique => true
    end

    say "Create table 'static_permissions'.", true
    create_table(:static_permissions) do |t|
      t.column :identifier,         :string,    :limit => 100, :null => false
    end

    say "Add indexes on table 'static_permissions'.", true
    add_index :static_permissions, :identifier

    say "Add relation table roles <---> static_permissions", true
    create_table(:roles_static_permissions, :id => false) do |t|
      t.column :role_id,              :integer, :null => false
      t.column :static_permission_id, :integer, :null => false
    end

    say "Add indexes on 'roles_static_permissions'", true
  end
end

Another requirement is that you require all the RBAC model files in your app/controllers/application.rb (see below: Gotchas).

5.4 Reference

At the moment, none of the acts_as_{user, role, static\permission} methods accepts any parameters. All the necessary information is taken from the classes you call the mixin method in via introspection.

5.4.1 Required Columns

Again, the required table columns:

5.4.2 Metaprogramming Done By ActiveRbac

As soon as the classes necessary for level 1 or 2 are registered, ActiveRbac will add the following to the user.

The method _is_anonymous? gets added to the user class and always returns false.

The relation has_and_belongs_to :roles gets added to the user class. The :class_name attribute of the macro will automatically be selected by the class that acts as the role class. The :uniq option is set to true.

The method has_role?(*identifiers) gets added to the user class. You can pass in one or more strings where each contains the identifier of a role. The method returns true iff one of the roles is assigned to the user object.

The method static_permissions gets added to the user class (level 2 only). The method returns all permissions received through the roles assigned to the user.

The method has_static_permission?(*identifiers) gets added to the user class. You can pass in one or more strings where each contains the identifier of a static permission. The method returns true iff one of the static permissions is granted to the user object through one of its role.

The role class will be augmented as follows.

The relation has_and_belongs_to :static_permission gets added to the role class. The :class_name attribute of the macro will automatically be selected by the class that acts as the static_permission class. The :uniq option is set to true.

The method has_static_permission?(*identifiers) gets added to the role class. You can pass in one or more strings where each contains the identifier of a static permission. The method returns true iff one of the static permissions is granted to the role object.

Validations for the identifier to be present and unique are added to the role class.

The static permission class is augmented as follows.

Validations for the identifier to be present and unique are added to the static permission class.

5.5 Gotchas

Make sure that you require all of your model files in app/controllers/application.rhtml with require_dependency. The order is not important.

require_dependency 'user'
require_dependency 'role'
require_dependency 'static_permission'

class ApplicationController < ActionController::Base
end

5.6 TODO

6 ActiveRbac Helpers

6.1 current user

When you mark a controller with acts_as_current_user_container then you can access the current user object using current_user in your views.