Usando o strong_parameters no Rails


Leia em 2 minutos

À partir do Rails 3.2.3 foi adicionada uma medida de segurança que marca todos os atributos de modelos do ActiveRecord como protegidos, em uma tentativa para impedir que incidentes como o do Github aconteçam novamente.

Mesmo com esta medida, que usa o método ActiveRecord::Base.attr_accessible, foram iniciadas discussões sobre outras maneiras de se resolver o problema. O Rails 4 virá com uma funcionalidade que permite filtrar quais parâmetros pode ser recebidos, mas que pode ser usada hoje mesmo em seu app através da biblioteca strong_parameters.

Configurando seu app

Se você quiser, pode desativar a whitelist dos atributos que podem ser atribuídos. Para fazer isso, basta editar o arquivo “config/application.rb”.

require File.expand_path("../boot", __FILE__)

require "rails"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "sprockets/railtie"

Bundler.require(*Rails.groups(:assets => %w[development test]))

module HOWTO
  class Application < Rails::Application
    config.active_record.whitelist_attributes = false
  end
end

Particularmente acho uma boa ideia continuar usando o ActiveRecord::Base.attr_accessible, principalmente se você terá vários perfis de atribuição, como o exemplo abaixo:

class User < ActiveRecord::Base
  DEFAULT_ATTRIBUTES = [:name, :email, :password, :password_confirmation, :username]

  attr_accessible *DEFAULT_ATTRIBUTES
  attr_accessible *DEFAULT_ATTRIBUTES, :role, :as => :admin
end

Uma outra motivação é que o ponto de entrada de dados pode mudar na aplicação, como background jobs e importadores de conteúdo, e nem sempre queremos que todos os atributos sejam definidos por atribuição em massa.

Na prática, isso significa que um usuário que é admin, pode facilmente definir atributos extras usando este novo perfil.

User.new(params[:user], :as => :admin)

Independente de sua escolha, você precisará incluir o módulo ActiveModel::ForbiddenAttributesProtection em cada modelo que pretende proteger. Supondo que você queira fazer isso no modelo User:

class User < ActiveRecord::Base
  include ActiveModel::ForbiddenAttributesProtection
end

Não é preciso dizer que isso pode ser extremamente chato de se fazer. Uma alternativa é incluir este módulo diretamente na classe ActiveRecord::Base. Crie um arquivo “config/initializers/strong_parameters.rb” com o conteúdo abaixo. Óbviamente, isso fará com que todos os modelos sejam protegidos.

ActiveRecord::Base.class_eval do
  include ActiveModel::ForbiddenAttributesProtection
end

Agora você pode definir no controller quais atributos são obrigatórios, além da lista de atributos permitidos durante a atribuição.

class SignupController < ApplicationController
  def create
    @user = User.create(user_params)
    respond_with(@user)
  end

  private
  def user_params
    params
      .require(:user)
      .permit(:name, :email, :password, :password_confirmation, :username)
  end
end

Perceba que estou usando o método SignupController#user_params para definir quais serão os atributos utilizados. Se um atributo não permitido for atribuído, uma exceção ActiveModel::ForbiddenAttributes será lançada. E caso o atributo params[:user] não seja enviado, uma resposta 400 Bad Request será retornada com o texto Required parameter missing: user.

Uma das coisas que não gostei desta abordagem foi ter justamente estas configurações em meu controller. Eu penso que isso introduz um ruído um tanto quanto desnecessário. Pensando em uma forma de como diminuir este ruído, decidi criar uma classe chamada PermittedParams, que contém todas essas definições. Além disso, sobrescrevi o método ActionController::Base#params para que ele use esta mesma configuração.

Primeiro vamos sobrescrever o método ActionController#params. Abra o arquivo “app/controllers/application_controller.rb” e adicione as linhas abaixo:

class ApplicationController < ActionController::Base
  private
  def params
    PermittedParams.new(super)
  end
end

Nossa classe PermittedParams irá delegar todas as chamadas para a implementação original, definindo apenas nossos filtros.

class PermittedParams < SimpleDelegator
  def user
    params
      .require(:user)
      .permit(:name, :email, :password, :password_confirmation, :username)
  end

  private
  def params
    __getobj__
  end
end

Agora, precisamos alterar o nosso controller. Para isso, remova o método SignupController#user_params, substituindo pela nossa nova implementação.

class SignupController < ApplicationController
  def create
    @user = User.create(params.user)
    respond_with(@user)
  end
end

Todos os filtros de outros modelos devem ser adicionados à classe PermittedParams. A grande vantagem desta abordagem é que fica muito simples ver todos os filtros existentes no sistema. Além disso, em apps com um grande número de filtros, é muito fácil fazer a organização desses métodos em módulos que podem ser incluídos na classe PermittedParams.