#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)
Hace más de tres años en el episodio 26 [verlo, leerlo] repasábamos la asignación masiva de atributos y veíamos cómo podía ser causa de vulnerabilidades de seguridad. Rails 3 ya ha visto la luz y por defecto incorpora muchas medidas de seguridad, pero no en este área. Los modelos de las aplicaciones Rails deben tener sus atributos protegidos contra la asignación masiva para evitar que usuarios malintencionados los puedan actualizar enviando peticiones POST al servidor. Los que no estén familiarizados con este problema deberían echar un vistazo al episodio 26, pero en esencia siempre que se crea o actualiza un modelo en los controladores utilizando la asignación masiva se debe utilzar attr_accesible
en el modelo para proteger los atributos que no queramos que sean modificables, o de lo contrario los usuarios podrían modificar cualquier atributo y esto puede llevar a problemas graves de seguridad.
Parece sencillo: sería simplemente añadir una llamada a attr_accesible
en todos los modelos pero esto presenta dos problemas potenciales. El primero puede ocurrir cuando estamos testando la aplicación. A veces podemos querer realizar una asignación masiva de atributos durante los tests y si los modelos están protegidos con attr_accesible
esto puede dificultarlo. Una solución es utilizar factorías, como vimos en el episodio 158 [verlo, leerlo].
El segundo problema es que attr_accesible
no es dinámico. Los atributos especificados como asignables para un modelo dado quedan grabados permanentemente y puede resultar difícil cambiar dichos atributos basándonos, por ejemplo, en los permisos del usuario actual. Este era el caso con Rails 2 pero en Rails 3 tenemos una nueva forma de hacer que los atributos sean dinámicos, y esto es lo que veremos en este episodio.
Nuestro Wiki
Nuestro wiki servirá como demostración de los atributos dinámicos. Este sitio tiene varios artículos, donde un artículo puede ser editado por cualquiera. En el formulario de edición hay una marca de selección junto a los campos correspondientes al nombre y el contenido que permite al usuario marcar el artículo como importante.
Si un artículo está marcado como importante su título aparece en rojo.
Modificaremos la aplicación para que sólo los administradores puedan cambiar la importancia de los artículos; los usuarios no administradores no deberían poder cambiar el campo important
. Sería bastante fácil cambiar el formulario para quitar la caja de selección y que sólo la viesen los usuarios administradores, pero esto no resolvería el problema porque aún sería posible que los usuarios puenteasen el formulario y enviasen una petición POST que modificase el campo important
del artículo.
La solución de este problema radica en las capas de modelo y controlador, más concretamente en las acciones create
y update
del controlador ArticlesController
, que es donde ocurre la asignación masiva. Un enfoque que podríamos adoptar sería proteger el atributo important
eliminándolo de la lista de parámetros a no ser que el usuario fuese un 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
El problema es que tenemos que acordarnos de hacer esto para todos y cada uno de los atributos que queramos proteger. Tampoco hay correlación con la llamada attr_accesible
del modelo. Todo sería mucho mejor si puediésemos hacer que attr_accesible
fuese dinámico.
Veamos la documentación de attr_accessible
para ver si podemos encontrar algo de ayuda. Un dato interesante es que attr_accessible
se encuentra incluído ahora en ActiveMode::MassAssignmentSecurity
, no en ActiveRecord, lo que le da la flexibilidad de poder ser incluido en cualquier clase. En la parte superior de la página de documentación hay un ejemplo de uso de ActiveModel::MassAssignmentSecurity
dentro de un controlador en lugar de un modelo, lo cual es una idea bastante buena. Lo más interesante, sin embargo, es un fragmento de código que muestra cómo podemos hacer que attr_accessible
sea dinámico redefiniendo el método mass_assignment_authorizer
.
def mass_assignment_authorizer admin ? admin_accessible_attributes : super end
El código anterior cambia el comportamiento de la aplicación dependiendo de si el usuario es administrador o no, que es exactamente lo que queremos hacer. Si redefinimos este método en nuestro modelos podremos cambiar los campos que podrán ser modificados mediante asignación masiva dependiendo de cualquier condición.
El modelo Article
tiene este aspecto:
class Article < ActiveRecord::Base attr_accessible :name, :content, :important end
Se trata de una clase bastante simple, con una única llamada a attr_accessible
con tres atributos. El atributo :important
es el que queremos que sea dinámico, lo que haremos redefiniendo mass_assignment_authorizer
:
class Article < ActiveRecord::Base attr_accessible :name, :content private def mass_assignment_authorizer super + [:important] end end
Para no anular el comportamiento por defecto, que consiste en devolver una lista de atributos, bastará con invocar a super
en nuestra versión de mass_assignment_authorizer
. No hace falta estar especialmente familiarizados con esto para utilizarlo, tan sólo tenemos que tener en cuenta que podemos añadir más atributos como se ha hecho arriba. Una vez que tenemos este parámetro extra podemos eliminarlo de la lista de parámetros en attr_accessible
.
Con esto cambios aún no habremos cambiado el comportamiento de la aplicación, pero podemos hacer que la accesibilidad de :important
sea dinámica definiéndola en una variable de instancia en lugar de hacerlo a nivel de clase. Añadiremos una variable a la clase que contendrá una lista de los atributos que deseamos que sean accesibles.
class Article < ActiveRecord::Base attr_accessible :name, :content attr_accessor :accessible private def mass_assignment_authorizer super + (accessible || []) end end
Cualquier parámetro que se pase a accesible
será añadido a la lista de atributos accesibles, lo que podremos usar en nuestros controladores. Modificaremos la acción update
para que añada el parámetro :important
sólo cuando el usuario actual tenga el privilegio de administración.
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 ahora arrancar la aplicación para ver si estos cambios surten efecto. Si iniciamos la sesión con una cuenta de usuario que no sea administrador y editamos un artículo marcándolo como importante, cuando seamos redirigidos de vuelta al artículo veremos que el titular sigue siendo negro lo que indica que el campo important
no ha sido modificado.
Si nos registramos como usuario administrador y volvemos a editar el artículo, veremos que esta vez el campo important
sí que se actualiza y el encabezado cambia de color.
Idealmente los administradores deberían poder editar cualquiera de los campos por lo que sería útil que accessible
soportase una opción :all
que nos permitiese marcar como modificables todos los atributos de un modelo. Podemos hacerlo modificando mass_assignment_authorizer
.
def mass_assignment_authorizer if accessible == :all self.class.protected_attributes else super + (accessible || []) end end
Ahora el método comprueba si accessible
vale :all
. Si es el caso tenemos que devolver algo que haga que todos nuestros atributos sean modificables. Lo ideal sería poder devolver un array vacío, pero por desgracia el objeto devuelto por mass_assignment_authorizer
es un objeto saneador, por lo que no nos vale esta posibilidad. Hemos hecho un pequeño hack que nos valdrá: devolvemos self.class.protected_attributes
, que se utiliza en el módulo MassAssignmentSecurity
para dar una lista negra de atributos que no se pueden modificar. Como en esta clase no estamos usando attr_protected
permitirá todos los atributos, que es justo lo que queremos hacer. Ahora podemos modificar ArticlesController
para que todos los atributos de Article
sean accesibles pasándole :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
Si lo probamos en la aplicación veremos que los adminstradores pueden editar el atributo important
.
En el controlador tenemos también que aplicar la opción accessible
a la acción create
. No funcionará si lo hacemos de la siguiente manera:
@article = Article.new(params[:article]) @article.accessible = :all if admin?
El motivo por el que este código no funciona es que la asignación masiva ocurre en la llamada a new
por lo que cuando establezcamos accessible
ya será demasiado tarde. Tenemos que separar la creación de un nuevo Article
de la asignación de sus atributos para encajar en medio la llamada a accessible
.
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
Podríamos querer hacer este comportamiento más abstracto para eliminar la duplicidad de código en las dos acciones, pero esto depende de cómo funcione nuestro sistema de permisos por lo que se deja como ejercicio para el lector. Un cambio que sí que haremos es extraer el método mass_assignment_authorizer
del modelo Article
para que se pueda usar en todos los modelos de la aplicación.
Moveremos este método a un inicializador, creando un fichero llamado accessible_attributes.rb
en el directorio /config/initializers
.
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
Este inicializador modifica ActiveRecord::Base
para que el comportamiento se aplique a todos los modelos. Nótese que seguimos invocando a attr_accessible
sin argumentos, lo que significa que el comportamiento por defecto será no permitir el establecimiento de ningún atributo mediante asignación masiva, y tendremos que añadir otra llamada a attr_accessible
para que dichos atributos sean modificables. Ahora podemos limpiar el modelo Article
para que quede así:
class Article < ActiveRecord::Base attr_accessible :name, :content end
Con esto concluimos este episodio. Hemos hecho que attr_accessible
sea completamente dinámico y podemos cambiar su funcionamiento basándonos en los permisos del usuario. Lo mejor de este enfoque es que por defecto todo queda protegido y sólo se permite el acceso específicamente en el código, lo que minimizará los problemas de seguridad derivados la asignación masiva