Skip to content

Instantly share code, notes, and snippets.

@andyh
Last active December 23, 2015 05:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andyh/54561365f16d7c65d22f to your computer and use it in GitHub Desktop.
Save andyh/54561365f16d7c65d22f to your computer and use it in GitHub Desktop.
Strong Parameters

Strong Parameters

What is it?

Replaces attr_accessible and attr_protected in Rails models, moving the responsibility for mass-assignment to the controller. Part of Rails 4 and available for Rails 3.2 as a gem.

Quick Example

class PeopleController < ActionController::Base
  def create
    Person.create(params[:person])
  end
end

This raises an ActiveModel::ForbiddenAttributes exception because it's using mass assignment without an explicit permit step.

Quick Example (continued)

class PeopleController < ActionController::Base
  def update
    person = current_account.people.find(params[:id])
    person.update_attributes!(person_params)
    redirect_to person
  end

  private
    def person_params    
      params.require(:person).permit(:name, :age)
    end
end

As long as there's a person key in the parameters this will be fine, otherwise it'll raise a ActionController::MissingParameter exception

Using a private method to encapsulate the permissible parameters is just a good pattern since you'll be able to reuse the same permit list between create and update. Also, makes it easier to specialise this method with per-user checking of permissible attributes.


Upgrading Rails 3 to use Strong Parameters

Demo upgrade of rails-api-identity

How does it actually work?

Live Coding Demo

Scalar Values

The permitted scalar types are String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile and Rack::Test::UploadedFile.

Nested Attributes and other Gotchas

Nested Parameters

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

Using accepts_nested_attributes_for

# permit :id and :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

e.g. has_many associations (Hashes with integer keys can be declared directly)

# To whitelist the following data:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}
 
params.require(:book).permit(:title, chapters_attributes: [:title])

Pros

  • Self Documenting, controllers are quite explicit about the data they are about
  • More flexible (easier to refactor into other objects)
    • Railscast Example

Model

class Topic < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts

  attr_accessible :name, as: :user
  attr_accessible :name, :sticky, as: :admin
end

Controller

def create
  @topic = Topic.new(params[:topic], as: current_user.try(:admin?) ? :admin : :user)
  if @topic.save
    redirect_to @topic, notice: "Created topic."
  else
    render :new
  end
end

Using Strong Parameters it becomes

private
def topic_params
  if current_user && current_user.admin?
					# allows all parameters if admin
    params.require(:topic).permit!
  else
    params.require(:topic).permit(:name)
  end
end

Moving it for re-use

			# /app/models/permitted_params.rb
class PermittedParams < Struct.new(:params, :user)
  def topic
    if user && user.admin?
      params.require(:topic).permit!
    else
      params.require(:topic).permit(:name)
    end
  end  
end

In application controller

			# /app/controllers/application_controller.rb
def permitted_params
  @permitted_params ||= PermittedParams.new(params, current_user)
end

In TopicsController

			# /app/controllers/topics_controller.rb
def update
  @topic = Topic.find(params[:id])
  if @topic.update_attributes(permitted_params.topic)
    redirect_to topics_url, notice: "Updated topic."
  else
    render :edit
  end
end

Cons

  • It doesn't handle all situations (but it's ruby so coding around it isn't too hard)
    • Example of whitelisting unknown attributes
params = ActionController::Parameters.new(user: { username: "john", data: { foo: "bar" } })
# let's assume we can't do this because the data hash can contain any kind of data
params.require(:user).permit(:username, data: [ :foo ])

# we need to use the power of ruby to do this "by hand"
params.require(:user).permit(:username).tap do |whitelisted|
  whitelisted[:data] = params[:user][:data]
end
# Unpermitted parameters: data
# => {"username"=>"john", "data"=>{"foo"=>"bar"}}

Testing

class UsersController < ApplicationController

   def update
      user = User.find(params[:id])
      user.update_attributes(UserParams.permit(params))

      respond_with user
   end

   class UserParams
      def self.permit params
         params.
         require(:user).
         permit(:first_name, :last_name)
      end
   end
end

Can be tested like so:

describe UsersController::UserParams do
    describe ".permit" do
      it "returns the cleaned params" do
        user_params = { first_name: "Kanye", last_name: "West" }
        params = ActionController::Parameters.new(user: { foo: "foo" }.merge(user_params))

        permitted_params = UsersController::UserParams.permit(params)
        expect(permitted_params).to eq(user_params.with_indifferent_access)
      end
    end
  end

Going Further

Signed Form

  • Form fields are signed, so no alteration of the fields are allowed
  • Form actions are signed. That means a form with an action of /admin/users/3 will not work when submitted to /users/3
  • Form views are digested. So if you remove a field from your form, old forms will not be accepted despite a valid signature
<%= form_for @user, signed: true do |f| %>
  <% f.add_signed_fields :zipcode, :state # Optionally add additional fields to sign %>

  <%= f.text_field :name %>
  <%= f.text_field :address %>
  <%= f.submit %>
<% end %>

Pundit

Pundit provides a set of helpers which guide you in leveraging regular Ruby classes and object oriented design patterns to build a simple, robust and scaleable authorisation system.

class TopicPolicy
  attr_reader :user, :topic

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

  def create?
    user.admin? or not topic.published?
  end
end

In your topics controller

def create
  @topic = Topic.new(params[:topic])
  authorize @topic
  if @topic.save
    redirect_to @topic
  else
    render :new
  end
end

Trivial to wrap up permitted parameters into the same objects used for authorisation

class TopicPolicy < Struct.new(:user, :topic)
  def permitted_attributes
    if user.admin? || user.owner_of?(topic)
      [:name, :sticky]
    else
      [:name]
    end
  end
end

class TopicsController < ApplicationController
  def update
    # ...
    if @topic.update_attributes(topic_attributes)
    # ...
  end

  private

  def topic_attributes
    params.require(:topic).permit(policy(@topic).permitted_attributes)
  end
end

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment