CanTango Permits

The Permit system for CanTango.

Permits are a way to partition Ability rules into logical units.

CanTango Permits includes:

  • a basic set of useful permits
  • a permit engine to execute the permits

The Permits systems comes with a few useful Permit types out-of-the-box, but you are free to develop your own to suit your needs.

Built-in permit types

The set of buit-in Permit types include:

  • User
  • UserType
  • AccountType
  • Special

The CanTango roles system adds Permit types for the following:

  • Role
  • RoleGroup

Custom permit types

You can easily customize or create your own types of Permits to suit your needs! The Permit system will introspect the ability candidate and see which Permits apply and then build and execute these permits automatically, merging all the resulting rule sets into one. The simplest way to create a custom permit types is to subclass the Attribute permit as shown below. To create more complex permit types that go beyond mapping to an attribute, please look at the code of any of the built-in permits (such as Attribute Permit) to gain an insight for how to do this.

Example of a custom permit type:

class MembershipPermit < CanTango::Permit::Attribute
  class Builder < CanTango::Permit::Attribute::Builder
    attribute :membership
  end

  def self.inherited(base_clazz)
    register base_clazz, :name => attribute_name(base_clazz)
  end

  # optional override of inference of attribute via class name
  attribute :membership
end

Permit system

The permit system iterates over all the registered types of permits and executes all registered permits of each type. The result is a set of Ability rules that work with the CanCan Ability mechanism.

Defining Permits and Licenses in your app

The app/permits folder will be added to Rails autoloading. The directory layout should be the following:

- /app
  - /permits
    - /permit
      - /role
         - editor.rb
      - /role_group
         - editors.rb
      - /user_type
         - admin.rb
      -/admin (account)
         - /role
           - editor.rb

Default :editor Role permit (scope less):

module Permit::Role
  class Editor < CanTango::Permit::Role
    def calc_rules
      can :edit, Post
    end

    module Cache
      def calc_rules
        can :edit, Post
      end
    end
  end
end

The :editor Role permit applicable for the Admin scope. Typically the scope is linked to the account (but doesn't have to be).

module Permit::Admin::Role
  class Editor < CanTango::Permit::Role
    def calc_rules
      can :edit, Post if session[:edit_mode]
    end

    modes :no_cache
  end
end

Here the #modes macro is used to indicate that this Permit should only be executed for the :no_cache mode.

Another option is fx to wrap the Permit class inside the Account class namespace like this:

- /app
  - /models
    - /admin_account
      - editor_role_permit.rb
    - admin_account.rb
class AdminAccount < ActiveRecord::Base
  class EditorRolePermit < CanTango::Permit::Role
    tango_permit  :name => :editor, :type => :role, :ns => :admin

    def calc_rules
      can :edit, Post if session[:edit_mode]
    end

    modes :no_cache
  end
end

By default this is not supported out-of-the-box, hence you have to use the #tango_permit macro to tell CanTango exactly how to understand this custom class namespace layout.

Note: This is NOT a recommended approach.

Licenses

Licenses have their own namespace folder license inside the app/permits folder. The default (scope less) licenses are placed directly in the license folder.

- /app
  - /permits
    - /license
      - blogging.rb
      - /admin
        - blogging.rb

Default :blogging License (scope less):

module License
  class Blogging < CanTango::License
    module Cache
      def calc_rules
        can [:create, :edit], Post
      end
    end

    module NoCache
      def calc_rules
        can :publish, Post if session[:publishing] == :on
      end
    end
  end
end

The :editor License applicable for the Admin scope. Typically the scope is linked to the account (but doesn't have to be).

module License::Admin
  class Blogging < CanTango::License
    def calc_rules
      can :edit, Post
    end

    modes :all
  end
end

Install 'cantango-permits' gem

gem install cantango-permits

Or insert into Gemfile

gem 'cantango-permits'

Run bundler in a terminal/console from the folder of your Gemfile (root folder of app)

$ bundle

Usage

require 'cantango/permits'
require 'cantango/permit_engine'

Configuration

Turn on/off: Enable and disable types of permits and specific permits

CanTango.config.permits do |permits|
  # which types of permits to enable
  permits.types.enable :user_type, :account_type

  permits.enable_all_for :account_type
  permits.types.disable :user_type, :account_type
  permits.disable_for :user_type, :admin, :editor
end

Registration: Which permits have been registered (and for which types)

CanTango.config.permits do |permits|
  permits.registry_for :account_type # Registry for :account_type permits
  permits.registered_for :account_type # names of AccountType permits
  permits.all
  permits.show_all
end

Debug: Which permits allowed/denied specific actions for specific candidates to be taken

CanTango.config.permits do |permits|
    permits.allowed candidate, actions, subjects, *extra_args
    permits.denied candidate, actions, subjects, *extra_args
end

Permits Engine

The Permits engine can be configured as any other CanTango Engine: Use the on! and off! methods to enable to disable use of the engine. Use the mode= to set the execution mode.

CanTango.config.engine(:permits) do |engine|
  # toggle engine on/off
  engine.on!
  engine.on?
  engine.off!
  engine.modes.valid # => [:cache, :no_cache, :both]

  # set execution modes
  engine.modes.register :cache, :no_cache
  engine.modes.registered # => [:cache, :no_cache]
end

Permits finder

Will look up a particular registered Permit in the Permit registry (see Configuration). Permits are registered automatically by an inheritance hook. If you want to override this, you need to register your class directly (manually) with the the permits registry.

module CanTango::Finder::Permit
  class Base
    def initialize name, type
      @name, @type = [name, type]
    end
  end
end

Permits Factory

Builds and returns a list of all enabled permits of a specific type

module CanTango::Factory
  class Permits
    def initialize ability
      @ability = ability
    end

    # @return Array<Permit>
    def create
      permits.build
    end
  end
end

Categories

Categories are loaded from a Yaml file (by default categories.yaml). The loader CanTango::Loader::Categories is based on the CanTango::Loader::Yaml from cantango-core. The parser CanTango::Parser::Categories parses the yaml into a hash referencing constants (models).

Macros

The macro #tango_permit can be used to attempt to register a Permit class with the Permits registry based on naming conventions and options passed in.

class MySuperPermit < CanTango::Permit::Base
  tango_permit :name => :super, :type => :user_type, :account => :admin

  # the internals
end

Engine

module CanTango::Engine
  class Permits
    def initialize ability
      super
    end

    def calc_rules
      # push result of each permit type execution into main ability rules array
      permits.each_pair do |type, permits|
        perm_rules = executor(type, permits).execute!
        rules << perm_rules if !perm_rules.blank?
      end
    end

    def executor type, permits
      CanTango::Ability::Executor::PermitType.new self, type, permits(type)
    end

    def permits type
      @permits ||= permits_factory(type).build!
    end

    def permits_factory type
      @permits_factory ||= CanTango::Factory::Permits.new self, type
    end
  end
end

Design goals

This CanTango extension should have the least amount of dependencies on other extensions.

Contributing to Cantango Permits

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright (c) 2011 Kristian Mandrup. See LICENSE.txt for further details.