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 = falseand either put this in each model you want to use Strong Parameters with:
include ActiveModel::ForbiddenAttributesProtectionOr 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::ForbiddenAttributesProtectionUsage
First, either put this in each controller you want to use Strong Parameters with:
include ActionController::PermittanceOr 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::PermittanceThen in your controller:
def create
@deal = Deal.new permitted_params
# ...
endNext, 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
endWhen 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
# ...
endTo specify a different Permitter to use with a Controller, either provide a permitter_class method:
def permitter_class
PersonPermitter
endOr 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))
# ...
endAuthorizers
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
# ...
endWhen 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
endWith 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
endCreate 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
endCanCan(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
endFor 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
endIn each model you use CanCan(Can) with, you can add this into the class:
include CanCan::ModelAdditionsTo 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::ModelAdditionsRelease Notes
See the changelog.
Contributors
License
Permitters is released under the MIT license.