web-dev-qa-db-fra.com

Accès à current_user à partir d'un modèle dans Ruby on Rails

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?

68
knuton

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.

42
gtd

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.

36
Josh K

Le contrôleur doit indiquer à l'instance de modèle

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.)

29
Nathan Long

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.

13
Hesse

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

9
armchairdj

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
8
khelll

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.

5
keredson

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
5
jimfish

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.

4
Daniel Kristensen

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.

0
Markus