ruby-on-rails - method - routes in ruby on rails
Acceso a current_user desde dentro de un modelo en Ruby on Rails (10)
El controlador debe indicar 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 contarla.
def create
@item = Item.new
@item.current_user = current_user # or whatever your controller method is
...
end
Esto supone que Item
tiene un attr_accessor
para current_user
.
(Nota: primero publiqué esta respuesta en otra pregunta, pero acabo de notar que la pregunta es un duplicado de esta).
Necesito implementar un control de acceso detallado en la 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
. Sin embargo, los modelos en general no tienen acceso a los datos de la sesión. may_read
?( current_user
, attribute_name
)
Hay algunas sugerencias para guardar una referencia al usuario actual en el hilo actual, por ejemplo, en esta publicación de blog . Esto ciertamente resolvería el problema.
Los resultados de Google vecinos me aconsejaron que guardara una referencia al usuario actual en la clase de Usuario, lo que supongo fue pensado por alguien cuya aplicación no tiene que acomodar a muchos usuarios a la vez. ;)
Para resumir, me da la sensación de que mi deseo de acceder al usuario actual (es decir, los datos de la sesión) desde dentro de un modelo proviene de que lo estoy haciendo mal .
¿Puedes decirme cómo me equivoco?
Aunque muchos respondieron esta pregunta, solo 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 User si recuerda usar 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 plantear es que, si almacena estado en variables de clase o singleton, recuerde que está lanzando seguridad de subprocesos por la ventana.
Bueno, supongo que es que current_user
es finalmente una instancia de usuario, entonces, ¿por qué no agregas estos permisos al modelo de User
o al modelo de datos? ¿Quieres que se apliquen o consulten los permisos?
Mi suposición es que necesitas reestructurar tu modelo de alguna manera y pasar al usuario actual como un param, 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
Diría que tus instintos para mantener current_user
fuera del modelo son correctos.
Al igual que Daniel, soy todo para 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 correcta en el modelo, esto se puede manejar con una línea de before_filters
que se aplican a varias acciones. Sin embargo, si las cosas se vuelven más complejas, quizás desee implementar una capa separada (posiblemente en lib/
) que encapsule 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 web / ciclo de respuesta
Estoy usando el plugin Declarative Authorization, y hace algo similar a lo que mencionas con current_user
Utiliza un before_filter
para sacar a current_user
y almacenarlo donde la capa del modelo pueda acceder. Se ve como 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
Sin embargo, no estoy usando las características del modelo de Autorización declarativa. 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.
Hilo antiguo, pero vale la pena señalar que a partir de Rails 5.2, hay una solución integrada a esto: el singleton del modelo actual, cubierto aquí: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything
Me gustan los modelos flacos de controlador y gordos, y creo que la autenticación no debería 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 establecer al usuario actual como "global de solicitud larga". Esto se hace 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 llamando a Auth :: user (). Ver http://laravel.com/docs/4.2/security
¿Por qué si puedo pasar el usuario actual del controlador?
Supongamos que estamos creando una aplicación de blog simple con soporte multiusuario. Estamos creando un sitio público (y los usuarios pueden leer y comentar en publicaciones de blog) y 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 "los 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 fue limpio y simple. En el sitio de administración, sin embargo, se necesita algo más. Esta es la implementación básica para todos los controladores de administración:
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
Así que se agregó la funcionalidad de inicio de sesión y el usuario actual se guarda en un global. Ahora el 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 thread-safe
Extendamos otros modelos para aprovechar 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
El modelo de publicación se amplió para que parezca que solo hay entradas del usuario actualmente conectadas. ¡Cuan genial es eso!
El controlador de la publicación Admin podría verse más o menos así:
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 comentarios, la versión de administrador podría verse así:
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
En mi humilde opinión: esta es una manera poderosa y segura de asegurarse de que los usuarios puedan 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 estoy equivocado pero creo:
Se trata de la separación de 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 estén utilizando User.current o que puedan acceder a la aplicación desde una consola, etc.
Mi sensación es que el usuario actual es parte del "contexto" de su modelo MVC, 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 todos estos " 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.
Siempre me sorprenden las respuestas de "simplemente no hagas eso" por parte de personas que no conocen la necesidad empresarial subyacente de la persona que pregunta. Sí, generalmente esto debería evitarse. Pero hay circunstancias en las que es apropiado y muy útil. Solo tuve uno yo mismo.
Aquí estaba 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 recorre la pila hacia atrás buscando un marco que responda 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.
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