Skip to content

joegoggins/sanction

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

91 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sanction: A permissions management Rails plugin

if @reader.has(:an_interest).over?(@simple_permissions_management)

"Keep reading" 
else
"Find a different plugin"
end

THIS IS A FORK

This is a fork of github.com/matthewvermaak/sanction, created to

  • facilitate the creation of sanction_ui, a front-end for Sanction.

  • make optimization tweaks to make Sanction scale to large datasets

It’s geared for development in my own specific apps and stuff, if you want stability, git clone from the latest tag at github.com/matthewvermaak/sanction

Matt’s code was merged into this fork on 2010-06-01, the main differences as of now 2010-06-04:

  • a check like Reader.has(:a_billion_rows) now utilizes MySQL indexes properly, the change is NOT fully integrated tho (test cases do still pass tho)

  • a different readme

  • my old legacy EagerHas code to be deprecated, which Matt has implemented a superior solution via :preload_roles

What is it

Sanction is a role based permissions management (rails) plugin.

It provides an intuitive set of extentions for ActiveRecord that can be used to lock down any facet of your app.

It can also be used for non-permissions based attribute assignment, aka

@a_user.grant(:pretty_pink_dogs)
@users = User.has(:pretty_pink_dogs)

If you need a front-end for it or a scaffold upon which to build a role assignment/permissions management front-end,

Check out SanctionUi

Features

  • Intuitive API calls that can be composed to form human readable sentences

  • Ability to mix app specific named_scopes + .find queries into permissions related queries easily

  • Easily gather sets of permission controlled objects matching particular role criteria.

  • Expressive config initializer file, like config/routes.rb for the permissions system, run ‘rake sanction:roles:describe` see the summary (like `rake routes`)

How it works

Sanction is mostly a bunch of named_scope’s that manipulate principals and permissionables, also known as subject and object, or user and resource.

It injects these named_scopes + methods into Principal and Permissionable ActiveRecord classes that expose the permissions management API.

The plugins’s DSL-ish syntax embodies several English syntaxes very closely.

who can do what

A person asks to “show the people that have some capability with a particular scope

Sanction asks:

In general @users = User.has(:some_capability).over(:some_resource)

For a type of resource @users = User.has(:editor).over(Magazine)

For a specific collection of resources @users = User.has(:can_enjoy).over(@the_economist,@nerdy_blog1)

In your application with your existing named_scopes and arbitrarly complex .find queries # active users with the :admin role @users = User.active.has(:admin) # active users who write for the economist @users = User.active.has(:writer).over(@the_economist)

true/false capability check

A person asks “Can a person do some action on a particular thing?

Sanction asks: (note the “?”)

@user.has?(:super_user)

@user.has(:can_read).over?(@page)

There are also the following functionally identical invocations that leverage ActiveRecord’s eager loading capabilities.

@user.eager_has?(:super_user)  
@user.eager_has_over?(:can_read,@page)

Note that these methods are only available on Principal class instances, to use them, @user needs to be loaded like:

@user = User.find(:first, :include => :eager_principal_roles)

resources that users have capabilities over

A person asks to see “The things that a person has a particular capability over

Sanction asks: @magazines = Magazine.for(@user).with(:can_edit)

@magazines = Magazine.published.for(@user).with(:editor).find(:all, :conditions => {:subject => “Programming”})

@tacos = Food.tastiest.for(@user).with(:can_eat).find(:all, :conditions => {:subject => “Annita’s Yum Yum Shop”})

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 or use the eager_has? or eager_has_over? methods (only available on Principal instances)

There are more examples at the end of this document.

Granting and Revoking Access

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

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.

If your principal and permissionable Active Record classes have underlying tables with string primary keys (rather than the Rails assumption of integer primary keys). You can also do this instead:

script/generate sanction string_ids=true

Be sure to rake db:migrate to produce the roles table.

Config

Example

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

  config.role :reader, 
    Person => Magazine, 
    :having => [:can_read],
    :purpose => "to limit who can read which Magazines"

  config.role :writer, 
    Person => Magazine, 
    :having => [:can_write], 
    :includes => [:reader],
    :purpose => "to limit who can write the magazines"

  config.role :editor, 
    Person => Magazine, 
    :having => [:can_edit, :can_create],  
    :includes => [:reader],
    :purpose => "to limit who can be the editor of Magazines"

  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 & Permissionables

Suppy an array of class names for each, each specified class will be injected with the appropriate API methods / scopes / and associations that constitute a Principal or 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 symbol

  • relationship: a hash defining a mapping between Principals and Permissionables to characterize behavior of role_name. Special tokens exist for mapping

    • :all

      The scope of the admin role spans over all Permissionable classes as in

      config.role :admin, Person => :all
      
    • :global The scope of the :super_user role is outside of the context of being “over” anything, it has :global scope as in

      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.

    • :purpose

      A string describing the role. (Used by perms management front-ends like sanction_ui)

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.

  • eager_has?(*roles) [Principal Instance Method Only]

    functionally identical to has?(*roles) only available on principal instances where the :eager_principal_roles association has been loaded, typically via something like

    @user = User.find(:first,:include => :eager_principal_roles)
    
  • has_all?(*roles)

    You can end a “sentence” with this method, allowing you to ask for ALL roles to be present. This is a more expensive operation, conducting a search on each role supplied as an argument. The nature of the _all methods prevents further chaining.

  • 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.

  • eager_has_over?(*roles, permissionable) [Principal Instance Method Only]

    functionally identical to .has(*roles).over?(permissionable) only available on principal instances where :eager_principal_roles has been eager loaded, differs in that the check can only be done on a single permissionable

  • over_all?(*permissionables)

    You can end a “sentence” with this method, allowing you to ask for a principal who has permission over ALL of these permisisonables. Again, this is subject to the _all exception, in that this method prevents further chaining.

  • 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 [Class Method Only]

    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.

  • with_all?(*roles)

    The _all version of with(*roles).

  • 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.

  • for_all?(*principals)

    The _all version of for(*principals).

  • authorize(role_name, principal)

    Must provide a role name and principal.

  • unauthorize(role_name, principal)

    Match the authorize call, to remove that entry.

  • total [Class Method Only] For the COUNT QUIRK.

Rake tasks

  • rake sanction:roles:describe This is like ‘rake routes` for the permissions system

  • rake sanction:roles:validate Check to see if any of the referenced principals or permissionables have any invalid foreign_keys

  • rake sanction:roles:cleanse Removes roles rows that are invalid

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

Misleading .count method

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.

Won’t play nice with Single Table Inheritance

Sanction will NOT work on principal classes that implement single table inheritance due to the funkyness associated with polymorphic relationships + STI in Rails.

Comments/Questions

Let us know matthewvermaak [at] gmail {dot} com peterleonhardt {at} gmail [dot] com joe.goggins {at} gmail [dot] com

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

About

A role based permissions authorization system for Ruby on Rails.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%