"Keep reading" else "Find a different plugin" end
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
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
-
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`)
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.
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)
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)
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.
@user.grant(:editor, @magazine) @user.revoke(:editor, @magazine) @user.revoke(:super_user)
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.
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
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.
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)
-
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.
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 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
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)
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.
Sanction will NOT work on principal classes that implement single table inheritance due to the funkyness associated with polymorphic relationships + STI in Rails.
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