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.