-
Notifications
You must be signed in to change notification settings - Fork 44
Permits
Permits are the core components executed by the Permit engine. Each Permit is an Ability on its own.
All Permits inherit from the generic class CanTango::Permit. CanTango comes with the following Permit types baked-in:
- Special permits
- Account type permits
- User type permits
- Role permits
- RoleGroup permits
It is quite easy to add more types of permits to fit your individual needs. You can even define and register your own Permit types to suit your needs and requirements (see Creating Custom Permits)!
The Permit engine also allows the use of Licenses, that are also defined as separate classes (see bottom). Licenses enable an easy way to mix-in rules, so they can be reused across permits (or even nested licenses!).
Finally, each Permit has access to Categories and some special Permit helper methods.
- localhost?
- publichost?
- category(name)
- any(reg_exp) - match model names
- license(name)
- licenses(*names)
- read_attribute(name)
- read_attributes(*names)
- edit_attribute(name)
- edit_attributes(*names)
- [relation]_of(*models, &block) - fx
owner_of - session
- request
- params
- controller
- domain
- cookies
- user
- account
The current built-in special permits are:
- SystemPermit
- AnyPermit
The SystemPermit is always put first in the permits evaluation list. The SystemPermit can be used to great effect to define "system level" rules and then abort further permit evaluation on some global condition.
The AnyPermit should be used for rules that should apply for any candidate, unless a previous "break out" occurs. Define your default rules here.
Note: Special permits are not currently in the flow (Nov 10), but will be re-inserted into the flow for the 1.0 release for sure. The functionality might change slightly, fx with respect to caching mode.
Account type permits are Permits that are run for a given type of account. Here is an example.
class AdminAccountPermit < CanTango::AccountPermit
def initialize ability
super
end
protected
def static_rules
can :read, [Comment, Post, Article]
can :create, Article
end
endThe rules of this permit will be executed for any ability candidate (fx a user) who has a direct reference to the Admin account via an #account method.
User type permits are Permits that are run for a given type of user. Here is an example.
class EditorPermit < CanTango::UserPermit
def initialize ability
super
end
protected
def static_rules
can :read, [Comment, Post, Article]
can :create, Article
end
endThe rules of this permit will be executed for any ability candidate who is a user and is an instance of the Editor class.
A Role permit is a specific Permit class for a particular role. If an ability candidate (for example, a user) has one or more roles, the Permit engine will try to load a Permit for each such role and add it to the list of permits to evaluate for that candidate.
Here is a Role permit for the role :user
# app/permits/roles/user_permit.rb
class UserRolePermit < CanTango::RolePermit
def initialize ability
super
end
protected
def permit_rules
can(:read, Comment)
can(:read, Post)
end
endAnd here a Role permit for the role :admin
# app/permits/roles/admin_permit.rb
class AdminRolePermit < CanTango::RolePermit
def initialize ability
super
end
protected
def dynamic_rules
can(:read, Article) do |article|
article.status == 'published'
end
end
endRole Group permits are very similar to Role permits except they operate with respect to the role groups of the ability candidate. A Role Group permit is loaded and evaluated in case the user has a matching role group.
Here is an example Role Group permit for the :admins group:
# app/permits/role_groups/admins_permit.rb
class AdminsRoleGroupPermit < CanTango::RoleGroupPermit
def initialize ability
super
end
protected
def dynamic_rules
can :edit, Article do
!localhost?
end
end
def static_rules
can :read, Article
cannot :access, User
end
endLicenses is a named set of rules that can be reused (mixed-in) into various Permits, such as Role permits and RoleGroup permits.
A License for :musicians is defined as follows:
class MusiciansLicense < CanTango::License
def initialize name
super
end
def static_rules
can(:read, Song)
can(:write, Tune)
can(:manage, Concerto)
cannot(:manage, Improvisation)
end
endAssume we have the licenses :musicians and :bloggers available. We can then use these licenses in a Permit as follows:
class UserRolePermit < CanTango::PermitEngine::RolePermit
def initialize ability
super
end
protected
def static_rules
author_of Article do |author|
author.can :write
end
licenses :musicians, :bloggers
end
endThe special #licenses method takes a list of licenses to include. The effect is the same as copy pasting the rules of these licenses directly into the place where #licenses is called!
Also notice the special #author_of DSL used in this example. This DSL can be used to describe Relational rules in a nice way.
In CanTango rules (or abilities) are defined the same way they are in CanCan.
See https://github.com/ryanb/cancan/wiki/Defining-Abilities.
Basic rules definition looks like
can :read, ArticleYou can restrict which records these rules applies to in two ways (we call them "dynamic rules" - depending on some conditions):
def permit_rules
can :read, Article, :author => @some_user
# some complex conditions can't be written using attributes hash
can :read, Article do |article|
article.authors.include?(@some_user) && Time.now > midnight
end
end
# Then somewhere in views:
user_can?(:read, @article)The main and only difference beetween CanTango and CanCan in the way rules are handled, is that CanTango can cache rules. CanCan always re-reads and executes all rules on every ability request.
Following example is typical in CanCan:
can :update, Project if user.lucky?In :cache mode, the above rule will loose its "dynamics" in CanTango:
can :update, ProjectThat is CanTango caches: ONLY rules and their conditional blocks internals.
After the rules are cached after the first ability request, #can? and #cannot? calls will directly address #cached_rules without re-evaluating the cached rules.
In CanTango, the above rule condition should be placed INSIDE a conditional block:
can :update, Project do |project|
user.lucky?
endPlacing the condition inside the block ensures that the condition will be remain relevant (not outdated!) for caching.
Note that there are 3 methods available for defining rules:
#static_rules#dynamic_rules#permit_rules
All methods act the same way. They 3 methods are there, simply to allow for a logical grouping of rules:
- static_rules - ONLY static rules (not depending on any dynamic condition)
- dynamic_rules - ONLY dynamic rules
- permit_rules - ANY kind of rule
If you're not planning to have a large number of static/dynamic rules just use #permit_rules as in the first example above. If you divide into static and dynamic rules however, it will be easy later to extract the static rules into a Permission store (see Permissions).
Since each Permit is in effect an Ability on its own, this lets you easily investigate which Permits allowed or denied a certain action for a specific ability candidate (typically a user or user account).
Debugging Example:
# First you must enable debug mode
CanTango.debug!
# And have the permit engine execute at least once
user_can? :read, Article
# Then you can debug the permits execution
CanTango.permits_allowed current_user, :read, Article
admin_can? :read, Article
CanTango.permits_denied admin_user, :publish, [Article, Post]
CanTango.permits_allowed admin_user, :write, Article
CanTango.clear_executed_permits! # reset as if no permits have been executed
user_can? :publish, Article
CanTango.permits_denied current_user, [:write, publish], [Article, Post]
guest_account_can? :publish, Article
CanTango.permits_denied guest_account, :publish, [Article, Post], optionsIn the near future we aim to enable more fine-grained debugging, where debugging levels can be defined for each engine similar to the current modes approach. Please let us know how you would like debugging to work to make it sufficiently transparent yet not cause too much "noise".
CanTango now support creating and registering your custom permits.
Here is an example, where we register a Membership Permit, simply by inheriting from CanTango::Permit and following a simple class naming convention: [name]Permit.
This feature is currently under development (Nov 8)
class MembershipPermit < CanTango::Permit
class Builder
...
end
module ClassMethods
def inherited(base_clazz)
CanTango.config.permits.register_permit_class membership_name(base_clazz), base_clazz, type, account_name(base_clazz)
end
def type
:membership
end
end
extend ClassMethods
def permit_name
self.class.permit_name self.class
end
def initialize ability
super
end
def permit?
super
end
def valid_for? subject
subject.memberships.include? membership_name
end
endYou also need to create a Permit builder:
class MembershipPermit < CanTango::Permit
class Builder < CanTango::PermitEngine::Builder::Base
def build
if memberships.empty?
debug "Not building any MembershipPermit"
return []
end
memberships.inject([]) do |permits, membership|
debug "Building MembershipPermit for #{membership}"
(permits << create_permit(membership)) if valid?(membership.to_sym)
permits
end.compact
end
def name
:membership
end
protected
def memberships
ability.candidate.respond_to?(:memberships) ? ability.candidate.memberships : []
end
def valid? membership
CanTango.config.memberships.registered.include? membership
end
end
endIn the Builder, we demonstrate how we could even expand the Configuration with a membership registry, in case we need to configure exactly which memberships should be valid for Permission purposes!
Imagine we have a User (the candidate):
class User
# return list of groups he is member of [:games, :techs]
def memberships
...
end
endThen the valid_for? subject method is central in order to determine if this Permit should be executed for a particular User (candidate) instance.
def valid_for? subject
subject.memberships.include? membership_name
endIn this example, if the call to memberships returns an Array that has a symbol matching the name of the Permit, this permit is executed. This example is very similar to the RolePermit implementation.
Example use:
class GamesMembershipPermit < MembershipPermit
def permit_rules
can :manage, Games
end
end
class TechMembershipPermit < MembershipPermit
def permit_rules
can :manage, Technology
end
endIn the near future we will improve the API and also enable this feature in the Permission engine, i.e. in the Permission store (typically the file permissions.yml).