Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
A role based permissions authorization system for Ruby on Rails.
Ruby
tree: 47421dfef9

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
app/models/sanction
generators/sanction
lib
tasks
test
MIT-LICENSE
README.rdoc
Rakefile
changelog
init.rb

README.rdoc

Sanction

A role based permissions management system designed to have an intuitive and useful API making an authorization system easy and painless. (Even Fun!)

Sanction works on the basis of principals and permissionables. Principals are those classes which can hold agency over a permissionable. In the initial configuration of Sanction, you declare your principal and permissionable classes. These do not have to be disjoint sets. After setting up the principals and permissionables, Sanction employs the concept of a role, as being an arbitrary token acting over the relationship of a set of principals mapped to a set of permissionables. The final stage of the Sanction configuration involves declaring these role definitions.

One of the key advantages of Sanction is to be able to easily access these principals or permissionables, depending on the interest of the query. This allows for generating a list of permissionables for a given principal with a certain role without having to iterate over a result set.

Enough with text, how about some quick examples:

How about getting all of the Users that are Magazine Editors?

User.has(:editor).over(Magazine) #=> [Users]

Or maybe you need to know if a specific user can read a page, without knowing their roles.

@user.has(:can_read).over?(@page) #=> true/false

Need to list all of the Magazines a user can edit?

Magazine.for(@user).with(:can_edit) #=> [Magazines]

Is this user a super user?

@user.has?(:super_user) #=> true/false

You can even throw your own named scopes into the mix, or chain on an ActiveRecord find()

User.active.has(:admin) #=> [Users]
Magazine.published.for(@user).with(:editor).find(:all, :conditions => {:subject => "Programming"}) #=> [Magazines]

How about giving and removing permissions?

@user.grant(:editor, @magazine)
@user.revoke(:editor, @magazine)
@user.revoke(:super_user)

Notice the grammer of the sentences. What you start with is what you end up with i.e., start your sentence with your desired objects. If you just need a true/false answer, throw a ? on the last method. How much simpler could it get?

There are more examples at the end of this document.

Features

- Intuitive API calls that can be composed to form sentences / questions based from principal or permissionable.
- Ability to chain custom scopes / finders into or after the Sanction role query api.
- Easily gather sets of principals or permissionables matching role criteria.

Install

After cloning / downloading, use:

script/generate sanction

This will stub out the config/initializers/sanction.rb used for configuration and will produce a migration for your roles table. Be sure to rake db:migrate to produce the roles table.

Testing

To run the Sanction test suite, which was generated with the help of plugin_a_week's plugin_test_helper, step into the vendor/plugins/sanction and run rake test.

Config

Example

Sanction.configure do |config|
  config.principals      = [Person, Login, User]
  config.permissionables = [Person, Magazine]

  config.role :reader, Person => Magazine, :having => [:can_read]
  config.role :editor, Person => Magazine, :having => [:can_edit],  :includes => [:reader]
  config.role :writer, Person => Magazine, :having => [:can_write], :includes => [:reader]
  config.role :owner,  Person => Magazine, :includes => [:editor, :writer]

  config.role :super_user, Person => :global
  config.role :boss,       Person => Person
  config.role :admin,      [Person, Login, User] => :all, :having => :anything
end

Details

Declaring Principals

In establishing the principal classes, you can supply an array of principals. Each of these principal classes will be injected with the appropriate API methods / scopes / and associations that constitute a Principal model within Sanction.

Declaring Permissionables

In establishing the permissionable classes, you can supply an array of permissionables. Each of these permissionable classes will be injected with the appropriate API methods / scopes / and associations that constitute a Permissionable model within Sanction.

Declaring Roles

In Sanction a role is defined as a name along with a relationship hash. When declaring this role additional options can also be declared.

config.role role_name, relationship, options
  • role_name: an arbitrary string/symbol

  • relationship: a hash entry, defined as a set of Principals mapping to a set of Permissionables. Special tokens exist for mapping to all permissionables(:all), or creating a role that is outside the context of permissionables (:global).

    • :all

      config.role :admin, Person => :all

    • :global

      config.role :super_user, Person => :global

  • Additional Options are:

    • :includes

      allows you to declare a set of roles that are included in this role. When using includes, you must “include” a role that has already been defined previously within the configuration, in order to inherit the permissions. You can not, therefore use a self referential include. Violating this will not cause an error, but rather, you will not inherit any permissions from that undefined role.

    • :having

      allows you to declare a set of finer grain permissions that this role responds to. These can be shared across roles, to allow for:

      config.role :reader, Person => Magazine, :having => [:can_read]
      config.role :editor, Person => Magazine, :having => [:can_read]

      In this example, asking for Person.has(:can_read) will yield both readers and editors

      • :anything

        config.role :admin, Person => :all, :having => :anything

        by using :having => :anything, any query to has() will return positive for that role, which can be useful for “super user” type roles.

API

Principal Methods

Each of the following methods are injected at the instance and class level.

  • has(*roles)

    provide any number of roles to look for. This is interpretted as asking looking for a principal that has ANY of these roles. Returns the principal objects matching. can be supplied :any, to wildcard the search for any role.

  • has?(*roles)

    the boolean form of has, returns true/false.

  • over(*permissionables)

    provide any number of permissionable instances or Klasses. This is interpretted as asking for principals having permissions over any of these permissionables. Returns the principal objects matching. can be supplied :any, to wildcard the search for any permissionable.

  • over?(*permissionables)

    The boolean form of over, returns true/false.

  • grant(role_name, permissionable = nil)

    Assign a role to a principal over an optional permissionable. Validated against the current Sanction::Role::Definition .

  • revoke(role_name, permissionable = nil)

    Remove a role. Use the same signature provided to grant.

  • total

    Only as a Class Method

    This method is a helper for the COUNT QUIRK mentioned below.

Permissionable Methods

Each of the following methods are injected at the instance and class level. (Except the total method)

  • with(*roles)

    provide any number of roles to look for. This is interpreted as asking for a permissionable governed by a principal with any of these roles. (READ: OR search). Returns the permissionable objects matching.

  • with?(*roles)

    The boolean form of with(*roles), returns true/false.

  • for(*principals)

    Provide any number of principals, for which you are searching for having a role/permission over the root permissionable.

  • for?(*principals)

    The boolean form for for(*principals), returns true/false.

  • authorize(role_name, principal)

    Must provide a role name and principal.

  • unauthorize(role_name, principal)

    Match the authorize call, to remove that entry.

  • total

    Only as a Class Method

    For the COUNT QUIRK.

Eager Loading / Preloading

Consider the use case of querying for a result set, and driling into that set with role based questions at the instance level. To avoid 1+N query related performance issues, you can:

@people = Person.find(:all, :preload_roles => true)

Subsequent calls to the Sanction API on those instances will no longer query the DB, but instead use the preloaded cache.

@people.each do |person|
  person.has?(:some_role) # Does NOT hit the DB
end

Rake tasks

  • rake sanction:role:describe

  • rake sanction:role:validate

  • rake sanction:role:cleanse

Many More Examples

Sanction.configure do |config|
  config.principals      = [Person]
  config.permissionables = [Person, Magazine]

  config.role :reader, Person => Magazine, :having => [:can_read]
  config.role :editor, Person => Magazine, :having => [:can_edit],  :includes => [:reader]
  config.role :writer, Person => Magazine, :having => [:can_write], :includes => [:reader]
  config.role :owner,  Person => Magazine, :includes => [:editor, :writer]

  config.role :boss,   Person => Person
end

Person.grant(:reader, Magazine.first)
  # => Grants the :reader role for all People over Magazine (1)

Person.find(2).grant(:editor, Magazine.find(2))
  # => Grants the :editor role for Person (2) over Magazine (2)

Person.find(3).grant(:owner, Magazine)
  # => Grants the :owner role for Person (3) over all Magazines

Person.has?(:any)
  # => Are there people who have any roles?
  # => true

Person.has?(:can_edit)
  # => Are there people who can edit?
  # => True

Person.has(:can_edit).over?(Magazine.first)
  # => Are there people who can edit Magazine(1) ?
  # => True

Person.has(:can_edit)
  # => List people who can edit
  # => Person (2,3)

Person.has(:editor)
  # => List people who have editor 
  # => Person (2,3)

Person.has(:owner)
  # => List people who have owner
  # => Person (3)

Person.has(:can_edit).over(Magazine.find(3))
  # => List people who can edit Magazine (3)
  # => Person (3)

Magazine.for(Person.find(3)).with(:can_edit)
  # => List the magazines that Person (3) :can_edit
  # => Magazine.all

Magazine.for(Person.find(3)).with(:can_edit).find(:all, :conditions => ["magazines.created_at > ?", (Time.now - 1.week)])
  # => List the magazines that Person (3) :can_edit with additional conditions.

Person.find(1).grant(:boss, Person.find(3))
  # => Grants Person (1) to be the boss over Person (3) [ Gratz ]

Person.has(:can_edit).over(Magazine.find(2)).for(Person.first).with(:boss)
  # => Returns the people who have editor over Magazine(2) and also have Person(1) as a boss

Person.first.has?(:editor)
  # => Check if Person(1) has :editor role
  # => false

Person.find(2).has?(:editor)
  # => Check if Person(2) has :editor role
  # => true

Person.find(2).has(:editor).over?(Magazine.first)
  # => Check if Person(2) has :editor role over Magazine(1)
  # => false

Person.find(2).has(:editor).over?(Magazine.find(2))
  # => Check if Person(2) has :editor role over Magazine(2)
  # => true

So a potential application code example might be:

  • In the controller

    # Find all magazines that the Person has some role over
    @person = Person.find(parms[:person_id])
    @magazines = Magazine.for(@person)
    @magazines_for_editing = Magazine.for(@person).with(:can_edit)

Quirks

Count

Performing a '.count' at the end of a Sanction query, with its implied count(*), can lead to misleading totals. The best thing of course is to:

.count(:all, :select => "DISTINCT tablename.primary_key")

so we have a helper method to do just this. Each principal/permissionable has a class method:

Person.total
Magazine.total
Magazines::Article.total

Append that at the end of any query:

Person.has(:editor).total

To get the accurate size.

Comments/Questions

Let us know matthewvermaak [at] gmail {dot} com / peterleonhardt {at} gmail [dot] com

Copyright © 2009 Matthew Vermaak & Peter Leonhardt, released under the MIT license

Something went wrong with that request. Please try again.