J'ai besoin d'implémenter un contrôle d'accès à grain fin dans une application Ruby on Rails. Les autorisations pour les utilisateurs individuels sont enregistrées dans une table de base de données et je pensais que cela il serait préférable de laisser la ressource respective (c'est-à-dire l'instance d'un modèle) décider si un certain utilisateur est autorisé à lire ou à écrire dessus. Prendre cette décision dans le contrôleur à chaque fois ne serait certainement pas très SEC.
Le problème est que pour ce faire, le modèle doit avoir accès à l'utilisateur actuel, pour appeler quelque chose comme may_read
? (current_user
, attribute_name
)</code>. Cependant, les modèles n'ont généralement pas accès aux données de session.
Il existe de nombreuses suggestions pour enregistrer une référence à l'utilisateur actuel dans le thread actuel, par exemple dans cet article de blog . Cela résoudrait certainement le problème.
Les résultats voisins de Google m'ont conseillé de sauvegarder une référence à l'utilisateur actuel dans la classe User, ce qui, je suppose, a été pensé par quelqu'un dont l'application n'a pas à accueillir beaucoup d'utilisateurs à la fois. ;)
Pour faire court, j'ai l'impression que mon souhait d'accéder à l'utilisateur actuel (c'est-à-dire les données de session) à partir d'un modèle vient de moi le faire mal .
Pouvez-vous me dire comment je me trompe?
Je dirais que votre instinct est de garder current_user
le modèle est correct.
Comme Daniel, je suis tout à fait pour les contrôleurs maigres et les gros modèles, mais il y a aussi une répartition claire des responsabilités. Le but du contrôleur est de gérer la demande et la session entrantes. Le modèle devrait pouvoir répondre à la question "L'utilisateur x peut-il y faire cet objet?", Mais il est absurde de faire référence à current_user
. Et si vous êtes dans la console? Et si c'est un travail cron en cours d'exécution?
Dans de nombreux cas, avec l'API d'autorisations appropriée dans le modèle, cela peut être géré avec une ligne before_filters
qui s'appliquent à plusieurs actions. Cependant, si les choses deviennent plus complexes, vous voudrez peut-être implémenter une couche distincte (éventuellement dans lib/
) qui encapsule la logique d'autorisation plus complexe pour éviter que votre contrôleur ne se gonfle et que votre modèle ne soit trop étroitement couplé au cycle de demande/réponse Web.
Bien que plusieurs personnes aient répondu à cette question, je voulais simplement ajouter mes deux cents rapidement.
L'utilisation de l'approche #current_user sur le modèle utilisateur doit être implémentée avec prudence en raison de la sécurité des threads.
Il est bon d'utiliser une méthode class/singleton sur User si vous vous souvenez d'utiliser Thread.current comme moyen ou de stocker et de récupérer vos valeurs. Mais ce n'est pas aussi simple que cela car vous devez également réinitialiser Thread.current afin que la prochaine demande n'hérite pas des autorisations qu'elle ne devrait pas.
Le point que j'essaye de faire est, si vous stockez l'état dans des variables de classe ou singleton, rappelez-vous que vous jetez la sécurité des threads par la fenêtre.
Travailler avec la base de données est le travail du modèle. Le traitement des demandes Web, y compris la connaissance de l'utilisateur pour la demande actuelle, est le travail du contrôleur.
Par conséquent, si une instance de modèle doit connaître l'utilisateur actuel, un contrôleur doit le lui dire.
def create
@item = Item.new
@item.current_user = current_user # or whatever your controller method is
...
end
Cela suppose que Item
a un attr_accessor
pour current_user
.
(Remarque - J'ai d'abord publié cette réponse sur ne autre question, mais je viens de remarquer que cette question est une copie de celle-ci.)
Je suis tout à fait pour les modèles maigres de contrôleur et de graisse, et je pense que l'authentification ne devrait pas casser ce principe.
Je code avec Rails depuis un an maintenant et je viens de PHP communauté. Pour moi, c'est une solution triviale de définir l'utilisateur actuel comme "global à la demande". Ceci est fait par défaut dans certains frameworks, par exemple:
Dans Yii, vous pouvez accéder à l'utilisateur actuel en appelant Yii :: $ app-> user-> identity. Voir http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html
Dans Lavavel, vous pouvez également faire la même chose en appelant Auth :: user (). Voir http://laravel.com/docs/4.2/security
Pourquoi si je peux simplement passer l'utilisateur actuel du contrôleur ??
Supposons que nous créons une application de blog simple avec un support multi-utilisateurs. Nous créons à la fois un site public (les utilisateurs anon peuvent lire et commenter les articles de blog) et le site d'administration (les utilisateurs sont connectés et ils ont un accès CRUD à leur contenu dans la base de données.)
Voici "les AR standard":
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
Maintenant, sur le site public:
class PostsController < ActionController::Base
def index
# Nothing special here, show latest posts on index page.
@posts = Post.includes(:comments).latest(10)
end
end
C'était propre et simple. Sur le site d'administration cependant, quelque chose de plus est nécessaire. Il s'agit d'une implémentation de base pour tous les contrôleurs d'administration:
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
La fonctionnalité de connexion a donc été ajoutée et l'utilisateur actuel est enregistré dans un global. Maintenant, le modèle utilisateur ressemble à ceci:
# 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 est désormais thread-safe
Étendons d'autres modèles pour en profiter:
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
Le modèle de publication a été étendu pour donner l'impression qu'il n'y a que les publications de l'utilisateur actuellement connecté. Comme c'est cool!
Le post-contrôleur administrateur pourrait ressembler à ceci:
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
Pour le modèle de commentaire, la version d'administration pourrait ressembler à ceci:
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
À mon humble avis: il s'agit d'un moyen puissant et sécurisé pour s'assurer que les utilisateurs peuvent accéder/modifier uniquement leurs données, en une seule fois. Pensez à combien de développeur a besoin d'écrire le code de test si chaque requête doit commencer par "current_user.posts.wwhat_method (...)"? Beaucoup.
Corrigez-moi si je me trompe mais je pense:
Il s'agit de séparer les préoccupations. Même lorsqu'il est clair que seul le contrôleur doit gérer les vérifications d'authentification, en aucun cas l'utilisateur actuellement connecté ne doit rester dans la couche contrôleur.
Seule chose à retenir: NE PAS en abuser! N'oubliez pas qu'il peut y avoir des employés de messagerie qui n'utilisent pas User.current ou que vous accédiez peut-être à l'application à partir d'une console, etc.
Ancien thread, mais il convient de noter qu'à partir de Rails 5.2, il existe une solution intégrée à cela: le modèle actuel singleton, couvert ici: https://evilmartians.com/ chronicles/Rails-5-2-active-storage-and-Beyond # current-everything
Eh bien, je suppose que current_user
est enfin une instance d'utilisateur, alors, pourquoi n'ajoutez-vous pas ces autorisations au modèle User
ou au modèle de données u voulez-vous que les autorisations soient appliquées ou interrogées?
Je suppose que vous devez en quelque sorte restructurer votre modèle et passer l'utilisateur actuel en paramètre, comme faire:
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
Je suis toujours étonné par les réponses "ne fais pas ça" de la part de gens qui ne connaissent rien au besoin commercial sous-jacent de l'interrogateur. Oui, cela devrait généralement être évité. Mais il y a des circonstances où c'est à la fois approprié et très utile. Je viens d'en avoir un moi-même.
Voici ma solution:
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
Cela fait reculer la pile à la recherche d'un cadre qui répond à current_user
. Si aucun n'est trouvé, il renvoie zéro. Il pourrait être rendu plus robuste en confirmant le type de retour attendu, et peut-être en confirmant que le propriétaire de la trame est un type de contrôleur, mais fonctionne généralement simplement.
Je l'ai dans une de mes applications. Il recherche simplement la session de contrôleurs actuelle [: user] et la définit sur une variable de classe User.current_user. Ce code fonctionne en production et est assez simple. J'aimerais pouvoir dire que je l'ai inventé, mais je crois que je l'ai emprunté à un génie de l'internet ailleurs.
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
J'utilise le plug-in d'autorisation déclarative, et il fait quelque chose de similaire à ce que vous mentionnez avec current_user
Il utilise un before_filter
tirer current_user
sortez-le et stockez-le là où la couche modèle peut y accéder. Ressemble à ça:
# 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
Cependant, je n'utilise pas les fonctionnalités du modèle d'autorisation déclarative. Je suis tout à fait pour l'approche "Skinny Controller - Fat Model", mais mon sentiment est que l'autorisation (ainsi que l'authentification) est quelque chose qui appartient à la couche contrôleur.
Mon sentiment est que l'utilisateur actuel fait partie du "contexte" de votre modèle MVC, pensez à l'utilisateur actuel comme à l'heure actuelle, au flux de journalisation actuel, au niveau de débogage actuel, à la transaction actuelle, etc. Vous pouvez passer tout cela " modalités "comme arguments dans vos fonctions. Ou vous le rendez disponible par des variables dans un contexte en dehors du corps de fonction actuel. Le contexte local du thread est le meilleur choix que les variables globales ou d'une autre portée en raison de la sécurité du thread la plus simple. Comme l'a dit Josh K, le danger avec les sections locales de thread est qu'elles doivent être effacées après la tâche, quelque chose qu'un framework d'injection de dépendances peut faire pour vous. MVC est une image quelque peu simplifiée de la réalité de l'application et tout n'est pas couvert par celle-ci.