Object-oriented parameter authorization with Strong Parameters
Ruby CSS JavaScript CoffeeScript
Latest commit f5229dd Oct 8, 2014 @garysweaver garysweaver Merge pull request #7 from permitters/revert-6-master
Revert "Avoid to raise an exception if an associated entity is not found...
Permalink
Failed to load latest commit information.
gemfiles
lib
spec
.gitattributes
.gitignore
.rspec
.travis.yml
Appraisals
CHANGELOG.md
Gemfile
LICENSE
README-dev.md
README.md
Rakefile
permitters.gemspec

README.md

Build Status Gem Version

Permitters

Permitters are an object-oriented way of defining what request parameters are permitted using Strong Parameters. It is to Strong Parameters what ActiveModel::Serializers are to as_json/to_json.

Permitters also allow additional parameter authorization using an authorizing class, such as CanCan's Ability class or a custom authorizer.

The original version of the Permitters framework that this was based on was provided in a post by Adam Hawkins.

Installation

In your Rails app's Gemfile:

gem 'permitters'

Then:

bundle install

Strong Parameters

If you are using Rails 4.x, Strong Parameters is included and everything should be setup, so you can skip this section.

If you are using Rails 3.x, you'll need Strong Parameters:

gem 'strong_parameters'

Also in Rails 3.x, you probably will want to change this to false in config:

config.active_record.whitelist_attributes = false

and either put this in each model you want to use Strong Parameters with:

include ActiveModel::ForbiddenAttributesProtection

Or if you'd rather use Strong Parameters with all models, just put this at the bottom of your config/environment.rb or an initializer:

ActiveRecord::Base.send :include, ActiveModel::ForbiddenAttributesProtection

Usage

First, either put this in each controller you want to use Strong Parameters with:

include ActionController::Permittance

Or if you'd rather use Permitters with all controllers, just put this at the bottom of your config/environment.rb or an initializer:

ActionController::Base.send :include, ActionController::Permittance

Then in your controller:

def create
  @deal = Deal.new permitted_params
  # ...
end

Next, add a permitter for each controller that uses ActionController::Permittance in /app/permitters/.

For /app/controllers/deals_controller.rb, you would add a /app/permitters/deal_permitter.rb:

class DealPermitter < ActionController::Permitter
  # No premissions required to set this
  permit :name, :description, :close_by, :state

  # For non-scalar serialized attributes such as `Deal.serialize :favorite_colors, Array`
  permit({:favorite_colors => [], {}})

  # can pass `:authorize` with a permission:
  # This line allows user_id if the user can read the user specified
  # by the user_id. This only happens if it's present
  permit :user_id, :authorize => :read

  # same thing but automatically handles arrays of ids as well.
  # This line allows the attachment_ids if the user can manage all
  # the specified attachments
  permit :attachment_ids, :authorize => :manage

  # same thing as before but scopes this it to the
  # hash inside the line_items_attributes array
  #
  # line_items_attributes is permitted if every item in the array
  # is allowed.
  # 
  # This also only allows line items if the user can manage the parent
  scope :line_items_attributes, :manage => true do |line_item|
    # So you cannot manipulate line items outside the parent
    line_item.permit :id, :authorize => :manage 

    line_item.permit :name, :quantity, :price, :currency, :notes
    line_item.permit :product_id, :authorize => :read
  end
end

When you call permitted_params, this happens:

params.require(:deal).permit(:name, :description, :close_by, :state, :line_items_attributes => [:id, :name, :quantity, :price, :currency, :notes, product_id])

If the controller is namespaced, the permitter should have the same namespace, e.g. A:B:DealsController defined in app/controllers/a/b/deals_controller.rb requires A:B:DealPermitter defined in /app/permitters/a/b/deal_permitter.rb.

If you need to override the argument(s) to pass into the require, use resource in the permitter:

class DealPermitter < ActionController::Permitter
  resource :deal
  # ...
end

To specify a different Permitter to use with a Controller, either provide a permitter_class method:

def permitter_class
  PersonPermitter
end

Or to specify the permitter, use permitted_params_using(PermitterClass), e.g.:

def make_cotton_candy
  @cotton_candy = CottonCandy.new(permitted_params_using(A::B::CottonCandyPermitter))
  # ...
end

Authorizers

Permitters also allow additional parameter authorization using CanCan or a custom authorizer.

The authorizer class must implement initialize(user) and authorize!(permission, record) (like CanCan's Ability class).

The controller class can implement a method to return an instance of the authorizer class.

So, now let's add the authorize!:

def create
  authorize! :create, Deal
  @deal = Deal.new permitted_params
  # ...
end

When you call authorize!(:some_permission, YourModelClass) method, that method will raise an error if current_user doesn't have the appropriate permissions for those attributes for which :authorize is specified.

Adding a Custom Authorizer

To set this up, you'll need to add one of these into your app config:

config.action_controller.authorizer = 'SomeAuthorizer'

or:

config.action_controller.current_authorizer_method = 'current_authorizer'

If neither is specified, then if the controller calls authorize!(permission, record), nothing happens.

Without a Controller Method

Put this into your app config:

config.action_controller.authorizer = 'SomeAuthorizer'

Create a class lib/some_authorizer.rb that has an initialize(user) and authorize!(permission, record) methods:

class SomeAuthorizer

  def initialize(user)
    @user = user
  end

  def authorize!(permission, record)
    raise "You must login to create deals" if permission == :create && record == Deal && @user.name == 'guest'
  end
end
With a Controller Method

Put this into your app config:

config.action_controller.current_authorizer_method = 'current_authorizer'

and in your controller or ApplicationController return an instance of an authorizer from that method:

def current_authorizer
  @current_ability ||= ::SomeAuthorizer.new
end

Create a class lib/some_authorizer.rb that raises an error from authorize!(permission, record) if unauthorized:

class SomeAuthorizer
  def authorize!(permission, record)
    raise "Deals can only be created from 8-5pm" if permission == :create && record == Deal && (Time.new.hour < 8 || Time.new.hour >= 17)
  end
end
CanCan(Can)

(Note to self: this section can use more cans.)

CanCanCan/CanCan can integrate with the Permitters framework as an authorizer. To use CanCan(Can), you can put this into your app config:

config.action_controller.authorizer = 'Ability'
config.action_controller.current_authorizer_method = 'current_ability'

(Note: You can define either. If you don't set current_authorizer_method, it can try creating an instance of the authorizer using the current user. If neither are specified, nothing can happen when authorize!(permission, record) is called.)

CanCan(Can) can integrate with Authlogic, Devise, etc. to return a proper logged-in user, or you can return it however you wish from the current_user method in your controller. Just to provide a simple example, we can pretend the user was logged-in and can return a new User instance (which means you will need a User model):

class ApplicationController < ActionController::Base
  protect_from_forgery

  def current_user
    User.new
  end
end

For simplicity, we can write an "allow-everything" Ability in app/models/ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    can :manage, :all
  end
end

In each model you use CanCan(Can) with, you can add this into the class:

include CanCan::ModelAdditions

To use CanCan(Can) with all models, you can put this at the bottom of your config/environment.rb or an initializer:

ActiveRecord::Base.send :include, CanCan::ModelAdditions

Release Notes

See the changelog.

Contributors

License

Permitters is released under the MIT license.