2009-10-14 47 views
60

Necesito implementar un control de acceso detallado en una aplicación Ruby on Rails. Los permisos para usuarios individuales se guardan en una tabla de base de datos y pensé que sería mejor dejar que el recurso respectivo (es decir, la instancia de un modelo) decidiera si un determinado usuario puede leer o escribir en él. Tomar esta decisión en el controlador cada vez ciertamente no sería muy SECO.
El problema es que para hacer esto, el modelo necesita acceso al usuario actual, para llamar a algo como may_read? (current_user, attribute_name). Sin embargo, los modelos en general no tienen acceso a los datos de la sesión.Acceso a current_user desde un modelo en Ruby on Rails

Existen algunas sugerencias para guardar una referencia al usuario actual en el hilo actual, p. en this blog post. Esto ciertamente resolvería el problema.

Los resultados de Google vecinos me aconsejaron guardar una referencia al usuario actual en la clase de Usuario, que supongo que fue ideado por alguien cuya aplicación no tiene que acomodar a muchos usuarios a la vez. ;)

En pocas palabras, tengo la sensación de que mi deseo de acceder al usuario actual (es decir, datos de sesión) desde un modelo proviene de mí doing it wrong.

¿Me puede decir cómo me equivoco?

+0

Irónicamente, siete años después de que esto se le preguntó, "hacerlo mal" es 404ing. :) –

+0

"Pensé que sería mejor dejar que los recursos respectivos (es decir, la instancia de un modelo) decidan si un determinado usuario puede leer o escribir en él". El modelo puede no ser el mejor lugar para esa lógica. Las gemas como Pundit y Authority establecen un patrón de tener una clase separada de "política" o "autorizador" designada para cada modelo (y los modelos pueden compartirlas, si lo desea). Estas clases proporcionan formas de trabajar fácilmente con el usuario actual. –

Respuesta

36

yo diría que su instinto para mantener current_user del modelo son correctas.

Me gusta Daniel Estoy totalmente de controladores flacos y modelos gordos, pero también hay una clara división de responsabilidades. El propósito del controlador es administrar la solicitud y la sesión entrantes. El modelo debería ser capaz de responder la pregunta "¿Puede el usuario x hacer y para este objeto?", Pero no tiene sentido que haga referencia al current_user. ¿Qué pasa si estás en la consola? ¿Qué pasa si se está ejecutando un trabajo cron?

En muchos casos con la API de permisos correctos en el modelo, esto se puede manejar con una sola línea before_filters que se aplican a varias acciones. Sin embargo, si las cosas se vuelven más complejas, puede implementar una capa separada (posiblemente en lib/) que encapsula la lógica de autorización más compleja para evitar que su controlador se infle y evite que su modelo se acople demasiado a la solicitud/respuesta web. ciclo.

+0

Gracias, sus preocupaciones son convincentes. Tendré que investigar más a fondo cómo están funcionando algunos de los complementos de control de acceso para Rails. – knuton

+3

Los patrones de autorización rompen MVC por diseño, cualquier forma de evitar esto suele terminar peor y simplemente romper el MVC en esa área. Si necesita usar 'User.current_user' en el registro del thread actual, hágalo, simplemente haga lo posible para no abusar de él – bbozo

+0

. Creo que la idea de que los modelos no deberían saber qué usuario está accediendo a ellos es solo un esfuerzo mal concebido en perfección. NINGUNA PERSONA INDIVIDUAL AQUÍ LE RECOMENDARÍA QUE PASARA EN LA ZONA HORARIA al modelo en los parámetros del método. Eso sería estúpido Pero eso es lo mismo ... Tiene un objeto de tiempo, y la mayoría de las aplicaciones tienen el controlador de la aplicación para establecer la zona horaria, a menudo según quién esté conectado, y luego los modelos conocen la zona horaria. – nroose

8

Bueno, mi conjetura es que current_user es, finalmente, una instancia de usuario, por lo que, ¿por qué no u añadir estos permisos al modelo User o al modelo de datos u quieren tener los permisos para ser aplicado o consultado?

Mi conjetura es que u necesidad de reestructurar su modelo de alguna manera y pasar el usuario actual como un parámetro, como hacer:

class Node < ActiveRecord 
    belongs_to :user 

    def authorized?(user) 
    user && (user.admin? or self.user_id == user.id) 
    end 
end 

# inside controllers or helpers 
node.authorized? current_user 
4

estoy usando el plugin de autorización declarativa, y lo hace algo similar a lo Está mencionando con current_user. Utiliza before_filter para extraer current_user y almacenarlo donde la capa del modelo pueda acceder. Se parece a esto:

# set_current_user sets the global current user for this request. This 
# is used by model security that does not have access to the 
# controller#current_user method. It is called as a before_filter. 
def set_current_user 
    Authorization.current_user = current_user 
end 

No estoy usando las características del modelo de Declarative Authorization though. Estoy a favor del enfoque "Controlador flaco - Modelo de grasa", pero mi sensación es que la autorización (así como la autenticación) es algo que pertenece a la capa de controlador.

+0

autorización (así como autenticación) pertenece tanto a la capa de controlador (donde permite el acceso a una acción) como al modelo (permite el acceso a un recurso allí) – lzap

+2

¡no use variables de clase! Use Thread.current ['current_user'] o algo similar en su lugar. Eso es inseguro! ¡No olvide restablecer el usuario actual al final de la solicitud! – reto

+1

Esto puede parecer una variable de clase, pero en realidad es solo un método que establece el Thread.current. https://github.com/stffn/declarative_authorization/blob/master/lib/declarative_authorization/authorization.rb#L32 – evanbikes

5

Tengo esto en una aplicación mía. Simplemente busca la sesión actual de controladores [: usuario] y la establece en una variable de clase User.current_user. Este código funciona en producción y es bastante simple. Desearía poder decir que se me ocurrió, pero creo que lo tomé prestado de un genio de Internet en otro lado.

class ApplicationController < ActionController::Base 
    before_filter do |c| 
    User.current_user = User.find(c.session[:user]) unless c.session[:user].nil? 
    end 
end 

class User < ActiveRecord::Base 
    cattr_accessor :current_user 
end 
+0

¡Gracias! Si esto realmente funciona, eso significaría que se está generando una instancia separada de la clase User (por la cual realmente quiero decir una clase, no un objeto) para cada solicitud/sesión. ¿Es eso así? Usted dice que lo usa en producción, así que supongo que tiene que ser así, pero seguro que me parece extraño. – knuton

+0

El código anterior se usa en realidad junto con la configuración automática de los valores de created_by y updated_by de un registro en save, basado por supuesto en los datos de la sesión. En realidad, no estamos creando instancias múltiples, estamos actualizando una variable de clase. – jimfish

+0

Por qué estoy confundido: está configurando una variable de clase User.current_user. Pensé que sería una mala idea, ya que asumí que la misma instancia de la clase User (como en "Classes in Ruby" son objetos de primera clase, cada uno es una instancia de la clase Class ". Http: //www.ruby -doc.org/core/classes/Class.html) se usará en toda la aplicación de Rails. Si ese fuera el caso, este método obviamente conduciría a resultados no deseados si varios usuarios estuvieran activos simultáneamente. Como está utilizando esto en producción, supongo que funciona, pero eso significa que hay varias instancias de la clase User. – knuton

30

Aunque esta pregunta ha sido respondida por muchos simplemente quería agregar mis dos centavos rápidamente.

El uso del enfoque #current_user en el modelo de usuario debe implementarse con precaución debido a la seguridad del subproceso.

Está bien utilizar un método de clase/singleton en el Usuario si recuerda utilizar Thread.current como una forma de almacenar y recuperar sus valores. Pero no es tan fácil porque también tiene que restablecer Thread.current para que la próxima solicitud no herede los permisos que no debería.

El punto que estoy tratando de hacer es, si almacena estado en variables de clase o singleton, recuerde que está lanzando seguridad de subprocesos por la ventana.

+12

+10 si pudiera para esta observación. Guardar el estado de solicitud en cualquier método/variable de clase singleton es una muy mala idea. – molf

23

El controlador debe decirle a la instancia del modelo

Trabajar con la base de datos es el trabajo del modelo. La gestión de las solicitudes web, incluido conocer al usuario para la solicitud actual, es el trabajo del controlador.

Por lo tanto, si una instancia modelo necesita conocer al usuario actual, un controlador debe indicarlo.

def create 
    @item = Item.new 
    @item.current_user = current_user # or whatever your controller method is 
    ... 
end 

Esto supone que tiene un Itemattr_accessor para current_user.

(Nota - La primera vez que envió esta respuesta en another question, pero yo sólo he dado cuenta de que la pregunta es un duplicado de éste.)

+3

Usted hace un argumento basado en principios, pero, en términos prácticos, esto puede dar como resultado una gran cantidad de código adicional que configura al usuario actual en muchos lugares. En mi opinión, a veces el enfoque 'User.current_user' de thread-local hace que el resto del código sea más corto y más fácil de entender, a pesar de romper MVC. – antinome

+0

¡increíble! Tengo varios objetos anidados, entonces qué agregué los identificadores en las vistas (ocultas) y el attr_accessor en el modelo –

+0

@antinome Quizás el problema real, en el caso del OP, es que el modelo maneje las verificaciones de permisos. Se puede pasar una clase de "política" o "autorizador" por separado al modelo y al usuario actual y autorizar desde allí. Este enfoque significa que no tiene el modelo revisando los permisos en contextos en los que no desea que se revisen, como las tareas de Rake. –

0

Mi sensación es que el usuario actual es parte del "contexto" de su MVC modelo, piense en el usuario actual como la hora actual, la corriente de registro actual, el nivel de depuración actual, la transacción actual, etc. Podría pasar todas estas "modalidades" como argumentos en sus funciones. O lo hace disponible por variables en un contexto fuera del cuerpo de la función actual. El contexto local del subproceso es la mejor opción que las variables globales o de otro tipo debido a la seguridad de subprocesos más fácil. Como dijo Josh K, el peligro con los locales locales es que deben ser eliminados después de la tarea, algo que un marco de inyección de dependencia puede hacer por usted. MVC es una imagen un tanto simplificada de la realidad de la aplicación y no todo está cubierto por ella.

11

Estoy todo por delgado controlador & modelos de grasa, y creo que auth no debe romper este principio.

He estado programando con Rails desde hace un año y vengo de la comunidad de PHP. Para mí, es una solución trivial configurar al usuario actual como "global de solicitud de larga duración". Esto se realiza de forma predeterminada en algunos marcos, por ejemplo:

En Yii, puede acceder al usuario actual llamando a Yii :: $ app-> user-> identity. Ver http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html

En Lavavel, también puede hacer lo mismo al llamar a Auth :: user().Ver http://laravel.com/docs/4.2/security

¿Por qué si puedo pasar al usuario actual del controlador?

Supongamos que estamos creando una aplicación de blog simple con soporte multiusuario. Estamos creando tanto un sitio público (los usuarios pueden leer y comentar en las publicaciones de blog) como un sitio de administración (los usuarios están conectados y tienen acceso CRUD a su contenido en la base de datos).

Aquí están las "AR estándar":

class Post < ActiveRecord::Base 
    has_many :comments 
    belongs_to :author, class_name: 'User', primary_key: author_id 
end 

class User < ActiveRecord::Base 
    has_many: :posts 
end 

class Comment < ActiveRecord::Base 
    belongs_to :post 
end 

Ahora, en el sitio público:

class PostsController < ActionController::Base 
    def index 
    # Nothing special here, show latest posts on index page. 
    @posts = Post.includes(:comments).latest(10) 
    end 
end 

Eso estaba limpio & sencilla. En el sitio de administración, sin embargo, se necesita algo más. Esta es la implementación básica para todos los controladores administrativos:

class Admin::BaseController < ActionController::Base 
    before_action: :auth, :set_current_user 
    after_action: :unset_current_user 

    private 

    def auth 
     # The actual auth is missing for brievery 
     @user = login_or_redirect 
    end 

    def set_current_user 
     # User.current needs to use Thread.current! 
     User.current = @user 
    end 

    def unset_current_user 
     # User.current needs to use Thread.current! 
     User.current = nil 
    end 
end 

Se agregó la funcionalidad de inicio de sesión y el usuario actual se guarda en un servidor global. Ahora modelo de usuario se ve así:

# Let's extend the common User model to include current user method. 
class Admin::User < User 
    def self.current=(user) 
    Thread.current[:current_user] = user 
    end 

    def self.current 
    Thread.current[:current_user] 
    end 
end 

User.current ahora es seguro para subprocesos

Ampliemos otros modelos para tomar ventaja de esto:

class Admin::Post < Post 
    before_save: :assign_author 

    def default_scope 
    where(author: User.current) 
    end 

    def assign_author 
    self.author = User.current 
    end 
end 

modelo Post se amplió de manera que se siente como si solo hubiera entradas de usuarios actualmente conectadas. ¡Qué bueno es eso! Controlador de post

de administración podría ser algo como esto:

class Admin::PostsController < Admin::BaseController 
    def index 
    # Shows all posts (for the current user, of course!) 
    @posts = Post.all 
    end 

    def new 
    # Finds the post by id (if it belongs to the current user, of course!) 
    @post = Post.find_by_id(params[:id]) 

    # Updates & saves the new post (for the current user, of course!) 
    @post.attributes = params.require(:post).permit() 
    if @post.save 
     # ... 
    else 
     # ... 
    end 
    end 
end 

Para el modelo de comentario, la versión de administración podría tener este aspecto:

class Admin::Comment < Comment 
    validate: :check_posts_author 

    private 

    def check_posts_author 
     unless post.author == User.current 
     errors.add(:blog, 'Blog must be yours!') 
     end 
    end 
end 

mi humilde opinión: Esto es de gran alcance & manera segura de hacer Seguro que los usuarios pueden acceder/modificar solo sus datos, todo de una vez. Piense en cuánto necesita el desarrollador escribir el código de prueba si cada consulta necesita comenzar con "current_user.posts.whatever_method (...)"? Mucho.

corrígeme si me equivoco, pero creo que:

Se trata de la separación de las preocupaciones. Incluso cuando está claro que solo el controlador debe manejar las comprobaciones de autenticación, de ninguna manera el usuario actualmente conectado debe permanecer en la capa de controlador.

Lo único que debe recordar: ¡NO lo use en exceso! Recuerde que puede haber trabajadores de correo electrónico que no usen User.current o que tal vez accedan a la aplicación desde una consola, etc ...

5

Siempre me sorprenden las respuestas de "simplemente no hagas eso" de personas que saben nada de la necesidad empresarial subyacente del interrogador. Sí, generalmente esto debería evitarse. Pero hay circunstancias en las que es apropiado y muy útil. Solo tuve uno yo mismo.

Aquí fue mi solución:

def find_current_user 
    (1..Kernel.caller.length).each do |n| 
    RubyVM::DebugInspector.open do |i| 
     current_user = eval "current_user rescue nil", i.frame_binding(n) 
     return current_user unless current_user.nil? 
    end 
    end 
    return nil 
end 

Esto paseos de la pila hacia atrás en busca de un marco que responde a current_user. Si no se encuentra ninguno, devuelve nil. Se podría hacer más robusto al confirmar el tipo de devolución esperada, y posiblemente al confirmar que el propietario del marco es un tipo de controlador, pero generalmente funciona de maravilla.

+1

Realmente, como cuando? No puedo pensar en una sola razón por la cual alguien quiera hacer esto. Simplemente no lo hagas. No tiene sentido, es propenso a errores y hará que el código no se pueda usar (si el contexto no es una solicitud/respuesta HTTP). Hay soluciones mucho mejores, como configurar un 'attr_acessor' para el usuario actual de la persona que llama. Estoy bastante seguro de que casi todos los programadores experimentados te dirán que esta es una idea falsa, y no deberías hacerlo. Me gusta la novedad de tu respuesta, pero es una abominación. – Mohamad

+0

Mi caso fue capturar al usuario (si lo hubiera) que desencadenó una transacción financiera, sin importar cómo/qué ruta de código obtuvieron allí. – keredson

+2

@Mohamad, Sería bueno si pudiera elaborar más sobre el uso de 'attr_acessor' –

Cuestiones relacionadas