IIPolicy

A base policy to support management of authorization logic.

This gem is inspired by pundit specs.

Dependencies

  • ruby 2.3+
  • activesupport 5.0+

Installation

Add this line to your application's Gemfile:

gem 'ii_policy'

Then execute:

$ bundle

Usage

Prepare model:

class Item < ActiveRecord::Base
end

Prepare controller with current_user and call authorize:

class ItemsController < ActionController::Base
  def index
    @policy = authorize(ItemPolicy)
    @items = Item.all
  end

  def show
    @item = Item.find(params[:id])
    @policy = authorize(@item)
  end

  def current_user
    User.find(session[:login_user_id])
  end
end

Create policy that has methods corresponding with actions of controller:

class ItemPolicy < IIPolicy::Base
  def index?
    @user.admin?
  end

  def show?
    @user.admin? && @item.status != 'deleted'
  end
end

Controller

authorize lookups policy and calls it's method corresponding with current action. authorize takes following arguments:

# no argument (policy class is looked up using the name of controller class)
authorize

# instance (policy class is looked up using the name of instance's class)
authorize(@item)

# policy class
authorize(ItemPolicy)

# with extra context as second argument
authorize(@item, something: 'something')

Context is set to { user: current_user } in the controller by default. You can set other context you want by overriding policy_context:

class ItemsController < ActionController::Base
  def policy_context
    super.merge(something: 'something')
  end
end

When current user is not authoized, IIPolicy::AuthorizationError is raised. You can catch the error and render a special page using rescue_from:

class ItemsController < ActionController::Base
  rescue_from IIPolicy::AuthorizationError, with: -> { ... }
end

You can also create policy instance by yourself and check authorization using allowed method as follows:

# policy class
policy(ItemPolicy).allowed(:index?)

# instance
policy(@item).allowed(:index?)

Policy

Policy has following attributes:

class ItemPolicy < IIPolicy::Base
  def index?
    puts "user: #{@user}"
    puts "item: #{@item}"
    puts "context: #{@context}"
  end
end

policy = ItemPolicy.new(user: User.find(1), item: Item.find(1), something: 'something')
policy.allowed(:index?)
#=> user: #<User: ...>
#   item: #<Item: ...>
#   context: #<IIPolicy::Context user=..., item=..., something="something">

You can call another policy method in the same context:

class ItemPolicy < IIPolicy::Base
  def another_show?
    allowed(:show?)
  end
end

You can use policy for another instance by using policy:

class ItemPolicy < IIPolicy::Base
  def another_show?
    policy(@context.another_item).allowed(:show?)
  end
end

Callbacks

Following callbacks are available:

  • before_call
  • around_call
  • after_call

For example:

class ItemPolicy < IIPolicy::Base
  before_call do
    @something = @context.something
  end

  def index?
    @something == 'something'
  end
end

Coactors

You can define multiple coactors by using coact as follows:

# shared policy
class SharedPolicy < IIPolicy::Base
  def show?
    @user.admin?
  end
end

# base policy
class ItemPolicy < IIPolicy::Base
  coact SharedPolicy

  def show?
    @item.status != 'deleted'
  end
end

policy = ItemPolicy.new(user: User.find(1), item: Item.find(1))
policy.allowed(:show?)
#=> true

In this example, policy.allowed(:show?) is evaluated by SharedPolicy#show? && ItemPolicy#show?.

See coactive for more coact examples:

Lookup for policy

authorize and policy lookups policy class if the first argument of them is not a policy class. So the name of policy class should be composed of the base name of model or controller. For example:

class ItemPolicy < IIPolicy::Base
end

class Item
end

class ItemsController < ActionController::Base
end

IIPolicy::Base.lookup(Item)
#=> ItemPolicy

IIPolicy::Base.lookup(Item.new)
#=> ItemPolicy

IIPolicy::Base.lookup(ItemsController)
#=> ItemPolicy

Note that superclass of model or controller is also looked up until policy is found.

class ItemPolicy < IIPolicy::Base
end

class Item
end

class InheritedItem < Item
end

IIPolicy::Base.lookup(InheritedItem)
#=> ItemPolicy

IIPolicy::Base.lookup(InheritedItem.new)
#=> ItemPolicy

Logging

Policy supports instrumentation hook supplied by ActiveSupport::Notifications. You can enable log subscriber as follows:

IIPolicy::LogSubscriber.attach_to :ii_policy

This subscriber will write logs in debug mode as the following example:

Calling ItemPolicy#index? with #<IIPolicy::Context ...>
...
Called ItemPolicy#index? and return true (Duration: 0.1ms, Allocations: 9)

Contributing

Bug reports and pull requests are welcome at https://github.com/kanety/ii_policy.

License

The gem is available as open source under the terms of the MIT License.