Aegis - role-based permissions for your user models

Aegis allows you to manage fine-grained, complex permission for user accounts in a central place.

Installation

Add the following to your Initializer.run block in your environment.rb:

config.gem 'thelinuxlich-aegis', :lib => 'aegis'

Then do a

sudo rake gems:install

Alternatively, use

sudo gem install thelinuxlich-aegis

Changes in this fork

WARNING! Opinionated stuff! Now you can set the permission prefix, admin and crud verbs with your locale(default is ‘may’,‘admin’,‘read’,‘write’,‘update’ and ‘destroy’, respectively) Example: in locale/en.yml:

aegis:
  permission: 'should'
  admin: 'manage'
  read: 'access'
  write: 'insert'
  update: 'update'
  destroy: 'delete'

And then you can verify authorization with current_user.should_access_posts?

Also, there is a class method you can put on ApplicationController(or anything that extends ActionController::Base) to automatically add before_filter to all REST actions verifying authorization:

authorize_first!(:current_user, options)

First parameter is a string containing the method to access the current user on your favorite authentication gem, second parameter accepts :except => [ARRAY_OF_CONTROLLER_NAMES]

The user class now can call has_role :special_permissions => true, and it will add a has_many association with special_permissions to really customize what every role can access.

In Permissions class, you can add restful_permissions!(:except => [ARRAY_OF_CONTROLLER_NAMES]) and it will add the 4 crud verbs to verify permission with every role, first verifying if the current user has admin access then verifying in SpecialPermission association. Example: Imagine we have a controller called Posts. It will add the permission methods may_read_posts?, may_write_posts?, may_update_posts?, may_destroy_posts?, all customizable with locales.

For special permissions, you’ll also need a table for it. Create a migration like this:

class CreateSpecialPermissions < ActiveRecord::Migration
  def self.up
    create_table :special_permissions do |t|
      t.integer :user_id
      t.string :permission_module
      t.boolean :permission_read, :default => false
      t.boolean :permission_write, :default => false
      t.boolean :permission_destroy, :default => false
      t.boolean :permission_update, :default => false
      t.timestamps
    end
  end

  def self.down
    drop_table :special_permissions
  end
end

Until I fix the generator, the migration part is manual, unfortunately :(

Example

First, let’s define some roles:

# app/models/permissions.rb
class Permissions < Aegis::Permissions

  role :guest
  role :registered_user
  role :moderator
  role :administrator, :default_permission => :allow

  permission :edit_post do |user, post|
    allow :registered_user do
      post.creator == user      # a registered_user can only edit his own posts
    end
    allow :moderator
  end

  permission :read_post do |post|
    allow :everyone
    deny :guest do
      post.private?             # guests may not read private posts
    end
  end

end

Now we assign roles to users. For this, the users table needs to have a string column role_name.

# app/models/user.rb
class User
  has_role
end

These permissions may be used in views and controllers:

# app/views/posts/index.html.erb
@posts.each do |post|
  <% if current_user.may_read_post? post %>
    <%= render post %>
    <% if current_user.may_edit_post? post %>
      <%= link_to 'Edit', edit_post_path(post) %>
    <% end %>
  <% end %>
<% end %>

# app/controllers/posts_controller.rb
class PostsController
  # ...

  def update
    @post = Post.find(params[:id])
    current_user.may_edit_post! @post    # raises an Aegis::PermissionError for unauthorized access
    # ...
  end

end

Details

Roles

To equip a (user) model with any permissions, you simply call has_role within the model:

class User < ActiveRecord::Base
  has_role
end

Aegis assumes that the corresponding database table has a string-valued column called role_name. You may override the name with the :name_accessor => :my_role_column option.

You can define a default role for a model by saying

class User < ActiveRecord::Base
  has_role :default => :admin
end

All this will do, is initialize the role_name with the given default when User.new is called.

The roles and permissions themselves are defined in a class inheriting from Aegis::Permissions. To define roles you create a model permissions.rb and use the role method:

class Permissions < Aegis::Permissions
  role 'role_name'
end

By default, users belonging to this role are not permitted anything. You may override this with :default_permission => :allow, e.g.

role 'admin', :default_permission => :allow

Permissions

Permissions are specified with the permission method and allow and deny

permission :do_something do
    allow :role_a, :role_b
    deny :role_c
end

Your user model just received two methods called User#may_do_something? and User#may_do_something!. The first one with the ? returns true for users with role_a and role_b, and false for users with role_c. The second one with the ! raises an Aegis::PermissionError for role_c.

Normalization

Aegis will perform some normalization. For example, the permissions edit_something and update_something will be the same, each granting both may_edit_something? and may_update_something?. The following normalizations are active:

  • edit = update

  • show = list = view = read

  • delete = remove = destroy

Complex permissions (with parameters)

allow and deny can also take a block that may return true or false indicating if this really applies. So

permission :pull_april_fools_prank do
  allow :everyone do
    Date.today.month == 4 and Date.today.day == 1
  end
end

will generate a may_pull_april_fools_prank? method that only returns true on April 1.

This becomes more useful if you pass parameters to a may_...? method, which are passed through to the permission block (together with the user object). This way you can define more complex permissions like

permission :edit_post do |current_user, post|
  allow :registered_user do
    post.owner == current_user
  end
  allow :admin
end

which will permit admins and post owners to edit posts.

For your convenience

As a convenience, if you create a permission ending in a plural ‘s’, this automatically includes the singular form. That is, after

permission :read_posts do
  allow :everyone
end

.may_read_post? @post will return true, as well.

If you want to grant create_something, read_something, update_something and destroy_something permissions all at once, just use

permission :crud_something do
  allow :admin
end

If several permission blocks (or several allow and denies) apply to a certain role, the later one always wins. That is

permission :do_something do
  deny :everyone
  allow :admin
end

will work as expected.

Our stance on multiple roles per user

We believe that you should only distinguish roles that have different ways of resolving their permissions. A typical set of roles would be

  • anonymous guest (has access to nothing with some exceptions)

  • signed up user (has access to some things depending on its attributes and associations)

  • administrator (has access to everything)

We don’t do multiple, parametrized roles like “leader for project #2” and “author of post #7”. That would be reinventing associations. Just use a single :user role and let your permission block query regular associations and attributes.

Credits

Henning Koch, Tobias Kraze

link www.makandra.de