Permissive combines a model-based permissions system with bitmasking to create a flexible approach to maintaining permissions on your ActiveRecord models. It supports an easy-to-use set of methods for accessing and determining permissions, including some fun metaprogramming.
-
Get yourself some code. You can install as a gem:
gem install permissive
or as a plugin:
script/plugin install git://github.com/flipsasser/permissive.git
-
Generate a migration so you can get some sweet table action:
script/generate permissive_migration
rake db:migrate
First, define a few permissions constants. We'll define them in Rails.root/config/initializers/permissive.rb
. The best practice is to name them in a verb format that follows this pattern: "Object can DO_PERMISSION_NAME
".
Permission constants need to be int values counting up from zero. We use ints because Permissive uses bit masking to keep permissions data compact and performant.
module Permissive::Permissions
MANAGE_GAMES = 0
CONTROL_RIDES = 1
PUNCH = 2
end
And that's all it takes to configure permissions! Now that we have them, let's grant them to a model or two:
class Employee < ActiveRecord::Base
acts_as_permissive
validates_presence_of :first_name, :last_name
end
class Company < ActiveRecord::Base
validates_presence_of :name
end
Easy-peasy, right? Let's try granting a few permissions:
@james = Employee.create(:first_name => 'James', :last_name => 'Brennan')
@frigo = Employee.create(:first_name => 'Tommy', :last_name => 'Frigo')
@adventureland = Company.create(:name => 'Adventureland')
# Okay, let's do some granting. We'll start by scoping to a specific company.
@james.can!(:manage_games, :on => @adventureland)
# Now let's do some permission checking.
@james.can?(:manage_games, :on => @adventureland) #=> true
# We can also use the metaprogramming syntax:
@james.can_manage_games_on?(@adventureland) #=> true
@james.can_control_rides_on?(@adventureland) #=> false
# We can check for multiple permissions, too:
@james.can?(:manage_games, :control_rides) #=> false
# OR:
@james.can_manage_games_and_control_rides?
# Scoping can be done through any object
@frigo.can!(:punch, :on => @james)
@frigo.can_punch_on?(@james) #=> true
# And the permissions aren't reciprocal
@james.can_punch_on?(@frigo) #=> false
# Of course, we can grant global (non-scoped) permissions, too:
@frigo.can!(:control_rides)
@frigo.can_control_rides? #=> true
# BUT! Global permissions don't override scoped permissions.
@frigo.can_control_rides_on?(@adventureland) #=> false
# Likewise, scoped permissions don't bubble up globally:
@james.can_manage_games? #=> false
# And, last but not least, let's take all of those great permissions away:
@james.revoke(:manage_games, :on => @adventureland)
# We can revoke all permissions, in any scope, too:
@frigo.revoke(:all)
And that's it!
Permissive supports scoping at the class-configuration level, which adds relationships to permitted objects:
class Employee < ActiveRecord::Base
acts_as_permissive :scope => :company
end
@frigo.permissive_companies #=> [Company 1, Company 2]
Sometimes you want to overwrite all previous permissions in a can! method. That's pretty easy: just add :reset => true to the options.
@frigo.can!(:control_rides, :on => @adventureland, :reset => true)
There's a number of things I want to add to the permissive settings. At the moment, Permissive currently support scoping at the class level, BUT all it really does is add a has_many
relationship. @employee.can!(:do_anything)
will still work, as will @employee.can!(:do_something, :on => @something_that_isnt_a_company)
. That's pretty confusing to me. Adding more granular permissions might be cooler:
class Employee < ActiveRecord::Base
has_permissions do
on :companies
on :employees
end
end
which might yield something like
@employee.permissive_companies
# and
@employee.can_control_rides_in_company @adventureland
I'd also like to support a more intelligent grammar:
@james.can_punch? @frigo
@frigo.can!(:control_rides, :in => @adventureland)
Meta-programmed methods for granting and revoking would be cool, too:
@james.can_punch! @frigo
@frigo.cannot_control_rides_in! @adventureland
And while we're on the subject of metaprogramming, let's add some OR-ing to the whole thing:
@james.can_control_rides_or_manage_games_in? @adventureland
I'd also like to enable Permissive::Templates (pre-set permission groups, like roles):
administrator = Permissive::Template.named('Administrator')
@james.acts_like administrator
Next up! I currently use a manual reset to grant permissions through a controller. It would by great to DRY this stuff up and provide some decent path for moving permissions into HTML forms. Right now, it looks something like this:
<%= check_box_tag("employee[permissions][]", Permissive::Permissions::CONTROL_RIDES, @employee.can_control_rides?) %> Control rides
.. and in the controller:
def update
@employee.can!(params[:employees].delete(:permissions), :revert => true)
respond_to do |format|
...
end
end
Finally, I'd like to use the grant_mask
support that exists on the Permissive::Permission model to control what people can or cannot allow others to do. This would necessitate one of two things - first, a quick way of iterating over a person's granting permissions, e.g.:
<% current_user.grant_permissions.each do |permission| %>
<!-- Do something! -->
<% end %>
and second, write-time checking of grantor permissions. Something like this, maybe:
def update
current_user.grant(params[:employees][:permissions], :to => @employee)
end
which would allow the Permissive::Permission model to make sure whatever current_user
is granting to @employee, they're allowed to grant to @employee.
And that's it! Like all of my projects, I extracted it from some live development - which means it, too, is still in development. So please feel free to contribute!
Copyright (c) 2009 Flip Sasser & Simon Parsons, released under the MIT license