Permits

kristianmandrup edited this page Dec 21, 2012 · 13 revisions

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.

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

Special permits

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

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
end

The 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

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
end

The rules of this permit will be executed for any ability candidate who is a user and is an instance of the Editor class.

Role permits

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
end

And 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
end

Role group permits

Role 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
end

Licenses

Licenses 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
end

Assume 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
end

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

Defining Ability rules

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, Article

You 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, Project

That 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? 
  end

Placing the condition inside the block ensures that the condition will be remain relevant (not outdated!) for caching.

Rule method containers

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

Debugging Permits

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], options

In 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".

Creating custom Permits

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
end

You 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
end

In 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
end

Then 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
  end

In 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
end

In 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).