Développement Ruby Ruby on Rails 2013 | 10 | 16

Adavanced Rails authorizations with pundit

Pundit is a realy simple Ruby Gem, that does nothing more that you could have done yourself. Here is the power !

When updating to Rails 4 in it’s early days, you had no compliant authorization solutions.

Ryan Bates’s Cancan was the most used due to it’s flexibility and simplicity, but it is not Rails 4 compliant, and brings too much magic for me.

But is there something more flexible and simple ?

For one of my current job, I started to build a software that needs flexible right management. ( And also a People relations graph, but this is for another post ).

The requirement are :

In this article I’ll address only the first requirement, the second will be explained in the next article, while the last one does not need to be since it is obvious.

Let introduce the base models

user role relation diagram

It is a basic pattern. I don’t need to explain it. User can have many Roles and Role may be filled by many Users.

For each Role, I want to allow some activities. (IE: For accountant : create bill, update payment. For Team Manager : Plan Task for a member of his/her Team )

About Pundit

Pundit set some helpers to :

I recommend you to read Pundit’s README.md before going further, since I won’t explain what is already explained there.

The Activities

If found an interesting post from Derick Bailey about authorization patterns, putting words on some of my thoughts. Reading this is not required to go further, but I recommend to.

The activities is not a finite collection. In fact, from my point of view, activities are defined by the following set : All methods directly exposed to user except.

In the Ruby On Rails world as in many MVC frameworks, this mean all the methods available on controllers. (Authentication Activities is a Subset of this set, that has particularity to be allowed to all users.)

I do not want to have a collection of activities to maintain. I do not want to reify them through an Postgres table or a flat file.

I want that default behavior is to deny. This behavior should be overridden if you have the appropriate role.

Since I’am building a REST API, I want to scope each activity on his resource.

Example:

The Person resource expose through API it’s CRUD methods (We will not use HTTP verbs to authorized. I do not find clever to bind Authorization on the Transfer Protocol ).

I see Person activities as the following set :

[
  'person:create', 'person:update', 'person:delete', 'person:show', 'person:index', 
  # ...
]

The pattern is dead simple and interpolate as #{resource}:#{activity}

With all those postulates, I decided to store activities on Role through the Postgres Array Type. It will be an array of strings.

Meta-programming Right Management

Then I just had to teach Pundit the way I wan’t it to authorize activities :

class ApplicationController < ActionController::Base

  # Includes Authorization mechanism
  include Pundit

  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  # Globally rescue Authorization Errors in controller.
  # Returning 403 Forbidden if permission is denied
  rescue_from Pundit::NotAuthorizedError, with: :permission_denied

  # Enforces access right checks for individuals resources
  after_filter :verify_authorized, :except => :index

  # Enforces access right checks for collections
  after_filter :verify_policy_scoped, :only => :index


  private

  def permission_denied
    head 403
  end

end

class PersonPolicy < ApplicationPolicy

  class Scope < Struct.new(:user, :scope)
    def resolve
      scope
    end
  end
end
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def user_activities
    @user.roles.select(:activities).distinct.map(&:activities).flatten
  end

  def inferred_activity(method)
    "#{@record.class.name.downcase}:#{method.to_s}"
  end

  def method_missing(name,*args)
    if name.to_s.last == '?'
      user_activities.include?(inferred_activity(name.to_s.gsub('?','')))
    else
      super
    end
  end


  def scope
    Pundit.policy_scope!(user, record.class)
  end
end

This may need some explanations :

This is the ApplicationPolicy, it will be inherited by each resource, allowing to extend/ override Policies by resources.

I replaced the bulk Pundits question marked CRUD methods with some meta-programming to avoid code duplication, and tedious declarations.

Line 9 : It recovers allowed activities for the current user.

Line 13 : The current activity against which we authorize is inferred from the object’s class name ( the word record is used but i plan to change this, since record mean database record for me, but it could be any object ).

Line 17 : The (in)famous Ruby’s method missing !

Any unknown method ending with a question mark in the current scope will be interpreted as a check access right for this activity method.

Each time we want to check access right for an activity, it will lookup the allowed activities for the current user roles …

This is the way the most of work is done, with a few ruby lines of code.

To manage rights, all we need is to add the activity to user role, what could be more simple ?

The person resource

Since all the work is done by the ApplicationPolicy class, the PersonPolicy class is just there to include and override behaviors.

The application Controller

Nothing extravagant here. Just read comments. Note that when the right are insufficient, It just return a 403 HTTP Header, it is enough for a REST API and is the way I understand the RFC 2616.

The Rspec Tests. ( Behaviour Driven ? )

Just to show how it works and as proof , this is a dumb spec/test for the dumb person example.

require 'spec_helper'

describe PersonPolicy do
  subject { PersonPolicy }

  let(:person) { create(:person) }
  let(:user) { create(:valid_user) }

  context 'given user\'s role activities' do

    permissions :create? do
      context 'without person:create' do
        before(:each) { user.roles << create(:role, activities: %w(person:show)) }

        it 'denies' do
          should_not permit(user, person)
        end

      end

      context 'with person:create' do
        before(:each) { user.roles << create(:role, activities: %w(person:create person:show)) }

        it 'allow' do
          should permit(user, person)
        end

      end
    end

    permissions :update? do

      context 'without person:update role activity' do
        before(:each) { user.roles << create(:role, activities: %w(person:show)) }

        it 'denies' do
          should_not permit(user, person)
        end

      end

      context 'with person:update' do
        before(:each) { user.roles << create(:role, activities: %w(person:update)) }
        it 'allow' do
          should permit(user, person)
        end
      end

    end
  end
end

The test ouput the following :

rspec pundit output

Easy :)

In a further post i’ll detail how to apply read/write mask on object.