homeASCIIcasts

237: Usando attr_accessible dinamicamente. 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Rafael Carvalho

Há mais de três anos, no episódio 26 [assistir, ler] falamos sobre atribuição em massa e como isso pode causar problemas de segurança. O Rails 3 foi lançado e possui várias características mais seguras que já vêm habilitadas por padrão, mas não é o caso das atribuições em massa. Os modelos das aplicações Rails precisam ter seus atributos protegidos, para evitar que usuários mal intencionados atualizem esses atributos enviando-os para o servidor através de requisições POST. Se você não conhece esse problema, dê uma olhada no episódio 26, mas o importante é saber que sempre que você criar ou atualizar uma instância de um modelo nos seus controllers, usando atribuição em massa, é necessário usar o método attr_accessible nos modelos, para proteger os atributos que você não quer que sejam atualizados. Caso você não faça isso, os usuários poderão atualizar quaisquer atributos e isso pode levar a um enorme problema de segurança.

É simples chamar o métodoattr_accessible em cada modelo, porém existem dois problemas em potencial quando isso é feito. O primeiro problema ocorre quando você está testando sua aplicação. Algumas vezes você quer fazer atribuição em massa durante os testes e com os modelos protegidos pelo attr_accessible isso pode se tornar muito difícil. Uma solução para esse problema é usar factories, como foi mostrado no episódio 158 [assistir, ler].

O segundo problema é que o attr_accessible não é dinâmico. Os atributos que são especificados nele para um dado modelo são fixos. E alterar esses atributos baseado nas permissões dos usuários, por exemplo, pode ser difícil. Isso acontecia no Rails 2, mas o Rails 3 fornece uma nova maneira de termos atributos dinâmicos e vamos mostrar isso neste episódio.

Nosso Wiki Site

Para demonstrar os atributos dinâmicos, vamos usar um wiki site. Esse site tem alguns artigos e um artigo pode ser editado por qualquer pessoa. No formulário de edição, juntamente com os campos nome e conteúdo, está um checkbox que permite que um usuário marque um artigo como 'importante'.

Editing an article to mark it as important.

Quando um artigo é marcado como importante, seu título aparece em vermelho.

The page for an important article with a read headline.

Vamos modificar a aplicação de forma que somente os administradores possam alterar a importância de um artigo. Usuários não administradores não devem poder alterar o campo important. É fácil modificar o formulário e mostrar o checkbox somente para os administradores, porém isso não resolve o problema, pois ainda será possível que os usuários ignorem esse formulário e façam uma requisição POST que modifique o campo important de um artigo.

A solução para esse problema está nas camadas de modelos e controllers, especificamente nas actions create e update do ArticlesController, pois é onde a atribuição em massa acontece. Uma abordagem que poderíamos ter para proteger o atributo important, seria removê-lo dos params a menos que o usuário atual seja um administrador.

/app/controllers/articles_controller.rb

def update
  params[:article].delete(:important) unless admin?
  @article = Article.find(params[:id])
  if @article.update_attributes(params[:article])
    flash[:notice] = "Successfully updated article."
    redirect_to @article
  else
    render :action => 'edit'
  end
end

O problema dessa solução é que temos que lembrar de fazer isso para todos os atributos que queremos proteger. E também não vai existir uma correlação com a chamada ao attr_accessible no model. Seria muito melhor se pudéssemos ter um attr_accessible dinâmico.

Vamos dar uma olhada na documentação da API do Rails para ver se encontramos algo sobre o attr_accessible que possa ajudar. Uma coisa interessante mostrada na documentação é que o attr_accessible agora está no módulo ActiveModel::MassAssignmentSecurity, e não mais diretamente no ActiveRecord. Isso significa que ele pode ser incluído e usado em qualquer classe, o que é muito mais flexível. No topo da documentação, tem um exemplo de uso do ActiveModel::MassAssignmentSecurity diretamente no controller, em vez de estar no modelo, o que é realmente uma boa ideia. Uma coisa realmente interessante é o código que mostra como tornar o attr_accessible dinâmico sobrescrevendo o método mass_assignment_authorizer.

def mass_assignment_authorizer
  admin ? admin_accessible_attributes : super
end

O código acima muda o comportamento da aplicação baseado no fato do usuário ser um admin ou não. Isso é exatamente o que queremos fazer. Sobrescrevendo esse método nos nossos modelos, podemos alterar os campos que podem ser modificados pela atribuição em massa usando qualquer condição.

Nosso modelo Article atualmente está assim:

/app/models/article.rb

class Article < ActiveRecord::Base
  attr_accessible :name, :content, :important
end

Essa é uma classe simples somente com uma chamada ao attr_accessible passando três atributos. O atributo :important é o único que queremos tornar dinâmico e podemos fazer isso sobrescrevendo o método mass_assignment_authorizer.

/app/models/article.rb

class Article < ActiveRecord::Base
  attr_accessible :name, :content
  
  private
  def mass_assignment_authorizer
    super + [:important]
  end
end

Chamando super no método mass_assignment_authorizer estamos obtendo o comportamento padrão, o qual retorna uma "whitelist sanitizer". Você não precisa saber exatamente como isso funciona para poder usar. Só precisa saber que você pode adicionar mais atributos a ele como fizemos anteriormente. Uma vez que adicionamos esse parâmetro extra, podemos removê-lo da lista de parâmetros do attr_accessible.

As alterações que fizemos até agora não alteraram o comportamento da nossa aplicação toda, mas podemos tornar a acessibilidade do :important dinâmica agora, pois ele está definido em uma variável de instância em vez de estar no nível da classe. Vamos fazer isso adicionando uma variável à classe que vai conter uma lista de atributos que queremos tornar acessíveis.

/app/models/article.rb

class Article < ActiveRecord::Base
  attr_accessible :name, :content
  attr_accessor :accessible
  
  private
  def mass_assignment_authorizer
    super + (accessible || [])
  end
end

Qualquer parâmetro passado para o accessible vai ser adicionamdo à lista de atributos acessíveis e podemos usar isso em nossos controllers. Vamos modificar a action update para que seja adicionado o parâmetro :important somente se o usuário atual for um admin.

/app/controllers/articles_controller.rb

def update
  @article = Article.find(params[:id])
  @article.accessible = [:important] if admin?
  if @article.update_attributes(params[:article])
    flash[:notice] = "Successfully updated article."
    redirect_to @article
  else
    render :action => 'edit'
  end
end

Podemos iniciar nossa aplicação para ver se as alterações funcionaram. Se entramos na aplicação com uma conta que não é de admin, e editarmos um artigo, marcando esse artigo como importante, quando estivermos de volta à página de artigos, o título estará preto, indicando que o campo important não foi alterado.

The article is still not marked as important.

Se transformarmos a nossa conta em administrador e editarmos o artigo novamente, o campo important estará atualizado e o título mudará de cor.

The articles is now marked as important.

Os admins devem poder editar quaisquer campos, por isso seria útil se o accessible suportasse uma opção :all, que nos permitiria facilmente deixar todos os atributos do modelo editáveis. Podemos fazer isso modificando o mass_assignment_authorizer.

/app/models/article.rb

def mass_assignment_authorizer
  if accessible == :all
    self.class.protected_attributes
  else
    super + (accessible || [])
  end
end

O método agora verifica se o accessible é igual a :all. Se é igual, então precisamos retornar algo que tornará todos os atributos editáveis. Seria bom se pudéssemos retornar simplesmente um array vazio, mas infelizmente o objeto que é retornado pelo mass_assignment_authorizer é um objeto sanitizer, então isso não funciona. O jeito que temos trabalhado com isso é meio hack, mas funciona bem: retornamos self.class.protected_attributes. Isso é usado pelo módulo MassAssignmentSecurity para prover uma lista negra de atributos que não podem ser modificados. Como nós não usamos attr_protected nessa classe, isso vai permitir todos os atributos, que é justamente o que queremos aqui. Podemos agora modificar o ArticlesController para tornar todos os atributos do Article acessíveis, passando :all.

/app/controllers/articles_controller.rb

def update
  @article = Article.find(params[:id])
  @article.accessible = :all if admin?
  if @article.update_attributes(params[:article])
    flash[:notice] = "Successfully updated article."
    redirect_to @article
  else
    render :action => 'edit'
  end
end

Se testarmos isso na aplicação, vamos ver que os admins ainda podem editar o atributo important.

No controller, precisamos ainda aplicar a opção accessible para a action create. Se nós simplesmente aplicarmos como está não vai funcionar.

/app/controllers/articles_controller.rb

@article = Article.new(params[:article])
@article.accessible = :all if admin?

Não funciona porque a atribuição em massa acontece na chamada ao método new, então aplicamos o accessible tarde demais. Precisamos separar a criação de um novo Article da atribuição dos seus atributos e fazer a chamada ao accessible entre os dois.

/app/controllers/articles_controller.rb

def create
  @article = Article.new
  @article.accessible = :all if admin?
  @article.attributes = params[:article]
  if @article.save
    flash[:notice] = "Successfully created article."
    redirect_to @article
  else
    render :action => 'new'
  end
end

Você pode querer fazer isso de forma mais abstrata e remover a duplicação no código das duas actions, mas isso depende de como o seu sistema de permissões funciona, então vamos deixar por sua conta. Uma alteração que ainda vamos fazer é extrair o método mass_assignment_authorizer para fora do modelo Article, para que isso possa ser usado em todos os modelos da aplicação.

Vamos mover o método para um initializer. No diretório /config/initializers, vamos criar um novo arquivo chamado accessible_attributes.rb.

/config/initializers/accessible_attributes.rb

class ActiveRecord::Base
  attr_accessible
  attr_accessor :accessible
  
  private
  def mass_assignment_authorizer
    if accessible == :all
      self.class.protected_attributes
    else
      super + (accessible || [])
    end
  end
end

Esse initializer modifica o ActiveRecord::Base para o comportamento ser aplicado a todos os modelos. Note que ainda podemos chamar o attr_accessible sem argumentos. Isso significa que o comportamento padrão será de forma que nenhum atributo pode ser modificado através de atribuição em massa. E que vamos ter que adicionar uma chamada a attr_accessible em cada modelo que tenha atributos editáveis. Podemos agora organizar o model Article assim:

/app/models/article.rb

class Article < ActiveRecord::Base
  attr_accessible :name, :content
end

É isso. Nós fizemos o attr_accessible completamente dinâmico e podemos alterar seus atributos baseado nas permissões dos usuários. O bom disso é que tudo está bloqueado por padrão. E o acesso é dado somente onde especificarmos explicitamente no código. Isso torna a atribuição em massa um problema muito menor, pois por padrão todos os atributos estão seguros.