#237 Dynamic attr_accessible
- Download:
- source codeProject Files in Zip (108 KB)
- mp4Full Size H.264 Video (16.7 MB)
- m4vSmaller H.264 Video (10.7 MB)
- webmFull Size VP8 Video (25.9 MB)
- ogvFull Size Theora Video (24.3 MB)
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'.
Quando um artigo é marcado como importante, seu título aparece em vermelho.
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.
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:
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
.
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.
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.
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.
Se transformarmos a nossa conta em administrador e editarmos o artigo novamente, o campo important
estará atualizado e o título mudará de cor.
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
.
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
.
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.
@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.
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
.
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:
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.