Skip to content
kristianmandrup edited this page Oct 18, 2011 · 23 revisions

To setup an app with CanTango you must complete the following steps:

  • Install and Configure CanTango
  • Create and register a User model
  • Setup Authentication
  • Optionally
    • Configure use of Roles
    • Define Guest user logic
  • Create Permits
  • Define your Authorization logic in Views and Controllers
  • Optionally: Extract static Permit rules to Permission store

Install and Configure CanTango

  • Create a CanTango initializer

rails g cantango:install

Generates an initializer in config/initializers/cantango.rb

  • Turn on the engines you want to use in the CanTango initializer initializers/cantango.rb (see Configuration)
  • It's a good idea also to turn on debugging mode so you can follow the execution flow and determine any problems that might occur due to configuration errors.
  • You should also limit the types of permits used (permits.disable)
  • You might also want to shut off the cache and permission engine until you have your permits working.
CanTango.configure do |config|
  config.debug.set :on
  config.permits.disable :account, :special, :role_group
  config.engines.all :on

  # config.engine(:permission).set :off
  # config.engine(:cache).set :off
end

Create and register a User model

First you must have a User model. Use the tango_user macro in order to register a user class with CanTango. CanTango will then generate User APIs methods such as #user_can? for the User class, admin_can? for a registered Admin user class etc.

class User
  # register as a "user class" for CanTango
  tango_user
end

Setup Authentication

For each registered user class, you should ensure that you have a #current_[user class] method available for views and controllers. Devise generates such methods for each Devise model. For other authentication systems you might have to implement your own #current_[user class] methods.

Note: Some authentication systems (such as authlogic) only have a single #current_user method. If this is the case and you have multiple user classes, you will have to do some customization to have the extra #current_[user class] methods available.

Here is a very simplistic example of some authentication setup:

class ApplicationController
  # user_can? and cannot? methods require that the following method is available and
  # returns the current logged in user or nil if no user is logged in for this session
  def current_user
    session[:current_user]
  end
end

This method logic assumes that you have already have a login form (or similar/other authentication functionality) which populates session[:current_user] with a user object (or perhaps retrieving via the id stored in the session?).

Optionally configure a Role system

If you plan to allow users to have dynamic roles, you can easily integrate a role system. Otherwise you can have the User class act as a static role for the user objects.

If you use dynamic roles, you must ensure that a #roles_list method (which returns a list of symbols for each role) is available on any user class. Alternatively you can make a #has_role?(role) method available.

class User
  tango_user
  # returns/calculates list of symbols, where each symbol is a name of a role the user currently has
  def roles_list
    [:admin, :editor, :publisher]
  end

  # optional
  def has_role? role
    roles_list.include? role.to_sym
  end
end

You can also combine static and dynamic roles for more advanced cases! Simply define a permit both for the roles and the user class (see Permits for details).

Optionally define guest user logic

It can be useful to have your app always fallback to use a Guest user or Guest account. Otherwise you would have to implement specific logic to handle when there is no user (null), which tends to get ugly and messy. For details see Guest users and accounts

Example:

class User < ActiveRecord::Base
  # returns Guest user instance 
  def self.guest
    session[:guest_user] ||= Guest.new # create Guest model (usually not to be persisted!)
  end
end

This requires that you have a Guest model. In most cases you don't want to persist the Guest user (save to data store), hence it is recommended to have a separate Guest model, also to ensure you have the Guest logic in its own container. You can also define the logic to create a Guest user directly in the CanTango Configuration.

Note that CanTango also generates methods in the form of #session_[user_class], fx session_user which returns the current_user or if no user is logged in, falls back to retrieve the Guest user. This is very useful in order to always ensure you have a user object at hand. This allows a Guests to change language etc. and have this state stored on the Guest user instead of doing custom 'null object' case logic and store this data directly in the session, an anti-pattern!

You should normally have a "hook" when a user logs in, which clears the session for the Guest user (unless you want to reuse that guest user in case the user logs out again).

Guest user model

In order to have the Guest model simulate a normal Rails model, without being persisted, you can include and extend specific ActiveModel modules. Here an example:

class Guest
  extend  ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  def initialize
    # basic config of guest user!
  end

  def create options = {}
    # new instance and set valid attributes?
  end

  def has_role? role
    role.to_sym == :guest
  end

  def persisted?
    false
  end
end 

Create Permits

It is recommended that you start off by using Permits to define your ability logic. Permits are more flexible and easier to debug. You can then later extract your static Permit rules into a Permissions store. Permits are classes stored in app/permits (see Permits. You can use the Permit Generators to help generate various kinds of permits.

Example use of permit generators:

rails g cantango:user_permit User --read all --edit Comment
rails g cantango:user_permit Guest --read all --not_write Article
rails g cantango:role_permit Admin --manage all

Example User type permit:

class UserPermit < CanTango::UserPermit
  def initialize ability
    super
  end

  protected

  def permit_rules
    can :read, Article
    cannot :write, Article
    can :manage, [Post, Comment]
  end
end

This permit would apply for any user that is an instance of the User model (class).

Example role permit:

class GuestRole < CanTango::RolePermit
  # ...
  def permit_rules
    can :read, :all
  end
end

This permit would apply for any user that has a role :guest from calling the #roles_list method.

Define your Authorization logic in Views and Controllers

The User APIs and User Account APIs are available in your Views and Controllers. Start out simple, in order to test that everything is configured and works correctly. CanTango comes with some convenient debugging helper methods to help you determine if your permits work as expected. Note: You can also debug via your specs or test suite.

Now lets scaffold an Article model, with a REST controller and views:

rails g scaffold Article title:string description:text

class ArticlesController < ApplicationController
  # for simple demo simulate a user is logged in!
  def index
    # remove the line below to have no current user and thus simulate a guest (visitor)
    session[:current_user] = User.first

    # print permits that were registered correctly
    CanTango.debug_permits_registry

    # print all permits that allow/deny that user to perform that action/ability
    CanTango.debug_ability(current_user, :read, Article)
  end

Example view with CanTango Access Control logic in place:

# views/articles/index.html.erb
  <%= Article.all.inspect if user_can?(:read, Article) %>
  <%= link_to "Edit", edit_article_path(Article.first) if user_can?(:edit, Article) %>

Optionally: Extract static Permit rules to Permission store

After you have ensured that your Permits work, you can convert your static Permit rules to permissions stored in a Permission store. Try out the default a yaml store: config/permissions.yml. (See Permissions) The yaml permission store file should already be available if you used the cantango:install generator without any arguments.

Advanced ability rules that require dynamic logic must be defined using a block construct. Dynamic rules can thus only be defined as ruby code and hence only using Permit classes.

Example of dynamic rule:

def permit_rules
  can :edit, Project |project|
    project.manager == user && project.published?
  end
end

This kind of dynamic rule can NOT be stored in a permissions file! When you extract static Permit ability rules, do it in small steps, and make sure that your ability logic still works as expected after each step!

Example permission store:

# config/permissions.yml
roles:
  guest:
    can:
      read: Article
user_types:
  user:
    can:
      read: Article
      manage: [Post, Comment]
    cannot:
      write: Article

To test the permissions, you can now disable the UserPermit like this.

class UserPermit < CanTango::UserPermit
  def initialize ability
    super
    disable!
  end
end

You can also disable permits from the cantango initializer like this:

config.permits.disable_for :user, [:user, :admin]

This disables the User type permits UserPermit and AdminPermit.

If you manage to migrate all your Permits to a Permission store you should disable the Permits engine completely:

config.engine(:permit).set :off or config.permit_engine.set :off

If your ability logic works without the permit engine enabled you can then delete your permit classes.

Next, go to Quickstart CanTango with Devise to see how to adjust this example for use with the Devise authentication system. The next quickstart also demonstrates other more advanced use cases.