/just_rights

A simple way to manage an arbitrary set of permissions on a ActiveRecord or other resources.

Primary LanguageRubyMIT LicenseMIT

JustRights

A simple way to manage an arbitrary set of permissions on a ActiveRecord or other resources.

Example

Capabilities are arbitrary (read, comment, close) and will be saved as a bitmask in a magic column called rights in the database.

class Post < ActiveRecord::Base
  permissions :read, :comment, :close
end

Methods

This will add the following methods on your class:

p = Post.new
p.set_permissions = :read, :comment

You can compare permissions:

p.permission.capabilities            # => [:read, :comment]
p.permission.can? :read              # => true
p.permission.can? :close             # => false

You can see if a capability exists:

p.has_capability? :close             # => true
p.has_capability? :foobar            # => false

You can transfer permissions:

p2 = Post.new
p2.permission = p.permission

Admin Rights (Sticky Bit)

Sometimes you want admins to be able to do everything, you can create or alias a method on Post called sticky which will override all permissions:

class Post
  def sticky
    creator.admin?
  end
end

Now no matter what the permissions are set in the database they can do anything:

p.sticky                             # => true
p.set_permissions = :read, :comment

p.permission.capabilities = [:read, :comment, :close]
p.permission.can? :read              # => true
p.permission.can? :comment           # => true
p.permission.can? :close             # => true

Helpers Methods for Nested Attributes

Saving permissions from views, there are additional method helpers to aid in the creation of permissions. These are array indices or attributes:

p.permission[:read]          # => false
p.permission[:read] = true   # => true
p.permission[:read]          # => true
p.permission.read            # => true

There is a simulation of the nested attributes:

p.permission_attributes = { :read => '0', :close => '1' }
p.permission[:read]         # => false
p.permission[:close]        # => true

So in the view you can do the following and it will update as expected:

<%= f.fields_for :permission do |p| %>
  <%= p.check_box :read %>
  <%= p.label :read %>

  <%= p.check_box :comment %>
  <%= p.label :comment %>

  <%= p.check_box :close %>
  <%= p.label :close %>
<%- end %>

Multiple Resources

You may also specify different resources that may have different permissions or capabilities. You don’t have to create a model for each one, you may collect them on a single model which may makes sense:

class Project
  permissions :attach, :modify, :delete,  :on => :files
  permissions :create, :order, :complete, :on => :todos
end

Here, these will be stored as todo_rights and file_rights in the database.

p = Project.new
p.set_file_permissions :attach, :modify
p.set_todo_permissions :create

p.file_permission.capabilities  # => [:attach, :modify]
p.file_permission.can? :attach  # => true
p.file_permission.can? :finish  # => false

p.todo_permission.capabilities  # => [:create]
p.todo_permission.can? :append  # => true
p.todo_permission.can? :finish  # => false
p.todo_permission.can? :attach  # => false

As you can see, the capabilities are separated, and do not crossover. The exception is if sticky is set on the project, it will be set for all underlying permissions.

p.sticky                        # => true
p.file_permission.capabilities  # => [:attach, :modify, :delete]
p.todo_permission.capabilities  # => [:create, :order, :complete]

Behind the scenes, JustRights creates new classes within the resource that they are defined:

Post::Permission
Project::FilePermission
Project::TodoPermission

In this way, all permissions classes should be name-spaced and not cause constant collisions unless you have a previously defined class of the same name. Currently there is no option to define the name of the column the bitmask is saved, you may alias_method or contribute :D

Permission System

If you include the PermssionSystem in your controllers you will gain View Helpers, prebuilt before_filters, and rights management variables.

class ApplicationHelper
  include PermissionSystem
end

Once you have done this you can setup a filter or another mechanism to setup the current user’s rights. For one right just do:

class ApplicationControlller
  before_filter do |controller|
    self.rights = current_user.permission
  end
end

Or in the example above with multiple resources:

class ProjectController
  before_filter do |controller|
    project = Project.find params[:id]
    grant_rights_for :files => project.file_permission, :todos => project.todo_permissions
  end
end

Which ever works for your application. Once you have rights set from your controller or view you can:

<%- if rights.can?(:read) %>
  Priviledged info here
<%- end %>

Or again, with multiple resources:

<%- if rights_for(:files).can? :modify %>
  render :partial => 'modify_files', :collection => @project.files
<%- end %>

Verify Access Filter

To ensure that the correct rights are secured before executing an action there are before_filters that you can setup with verify_access:

class SomeController
  verify_access :can? => :read, :deny => 'You do not have permission to read this file', :only => :show
end

This will throw a PermissionExtension::ForbiddenAccess exception with the deny messaging. The exception is setup so that it will generate a 403 HTTP error, which can be targetted by placing a file in public/403.html or using rescue_for. And you can pass any of the options you normally pass to filters.

You can also use multiple resource form (and if you add multiples it will match on any of them):

class ProjectController
  verify_access :can? => {:project => :modify}, :deny => 'You do not have permission to read this file'
end

View Helpers

There are also two helpers that can aid in creating rights-based links and submit buttons. These are added for you when you include PermissionSystem:

<%= link_by_rights 'Read', read_path, :can? => :read, :deny => 'You do not have permission to read this resource' %>
<%= link_by_rights 'Attach', attach_file_path, :can? => {:file => :attach}, :deny => 'You do not have permission to attach files' %>

This will generate a link to the read path if the User can read, or a link that will show an alert with the :deny message if they cannot.

Another is the submit button submit_by_rights which take the same options as link_by_rights.

Notes

Ordering

Ordering of the capability is significant, always append if you can. If you change the order of the rights, their values do not move with them. In this case, you may need to write migration code or take care of how you order your capabilities.

Memory or Data Capacity

Each capability is exponential (adds a power of 2), so make sure that your columns are large enough to handle all the different capabilities.

Copyright © 2009 Reid MacDonald <reid@laruby.com>, released under the MIT license