Доступ к current_user из модели в Ruby on Rails

Мне нужно реализовать мелкозернистый контроль доступа в приложении Ruby on Rails. Разрешения для отдельных пользователей сохраняются в таблице базы данных, и я думал, что было бы лучше, чтобы соответствующий ресурс (например, экземпляр модели) решил, разрешено ли определенному пользователю читать или записывать на него. Принимая это решение в контроллере каждый раз, конечно, не было бы очень СУХОЙ.
Проблема в том, что для этого модель нуждается в доступе к текущему пользователю, чтобы вызвать что-то вроде may_read? (current_user, attribute_name). Тем не менее, модели вообще не имеют доступа к данным сеанса.

Есть несколько предложений по сохранению ссылки на текущего пользователя в текущем потоке, например. в этот пост в блоге. Это, безусловно, решит проблему.

Соседние результаты Google посоветовали мне сохранить ссылку на текущего пользователя в классе User, хотя, по-моему, это был придуман кем-то, чье приложение не должно вмещать много пользователей одновременно.;)

Короче говоря, я чувствую, что мое желание получить доступ к текущему пользователю (т.е. данные сеанса) изнутри модели происходит от меня делает это неправильно.

Можете ли вы рассказать мне, как я ошибаюсь?

Ответ 1

Я бы сказал, что ваши инстинкты сохранить current_user вне модели верны.

Как и Дэниэл, я все для тощих контроллеров и толстых моделей, но есть также четкое разделение обязанностей. Целью контроллера является управление входящим запросом и сеансом. Модель должна иметь возможность ответить на вопрос "Может ли пользователь x сделать y этому объекту?", Но для него бессмысленно ссылаться на current_user. Что делать, если вы находитесь в консоли? Что делать, если выполняется работа cron?

Во многих случаях с API прав прав в модели это можно обрабатывать с помощью одной строки before_filters, которая применяется к нескольким действиям. Однако, если что-то становится более сложным, вы можете захотеть реализовать отдельный слой (возможно, в lib/), который инкапсулирует более сложную логику авторизации, чтобы предотвратить раздувание вашего контроллера и предотвратить слишком плотную связь вашей модели с веб-запросом/ответ.

Ответ 2

Хотя на этот вопрос ответили многие, я просто хотел быстро добавить свои два цента.

Использование подхода #current_user в модели пользователя должно выполняться с осторожностью из-за безопасности потоков.

Хорошо использовать метод класса /singleton для пользователя, если вы не хотите использовать Thread.current в качестве способа или хранения и получения ваших значений. Но это не так просто, потому что вы также должны reset Thread.current, чтобы следующий запрос не наследовал разрешения, которых он не должен.

То, что я пытаюсь сделать, заключается в том, что если вы сохраняете состояние в переменных класса или singleton, помните, что вы выбрасываете защиту потока из окна.

Ответ 3

Контроллер должен указать экземпляр модели

Работа с базой данных - это модельное задание. Обработка веб-запросов, в том числе знание пользователя для текущего запроса, является заданием контроллера.

Следовательно, если экземпляр модели должен знать текущего пользователя, контроллер должен сказать ему.

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

Это предполагает, что Item имеет attr_accessor для current_user.

(Примечание. Я впервые отправил этот ответ на еще один вопрос, но я только что заметил, что этот вопрос является дубликатом этого.)

Ответ 4

Я использую тощий контроллер и толстые модели, и я думаю, что auth не должен нарушать этот принцип.

Я кодировал Rails уже год, и я прихожу из сообщества PHP.. Для меня это тривиальное решение, чтобы установить текущего пользователя как "глобальный запрос-запрос". Это делается по умолчанию в некоторых рамках, например:

В Yii вы можете получить доступ к текущему пользователю, вызвав идентификатор Yii:: $app- > user- > . См. http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html

В Lavavel вы также можете сделать то же самое, вызвав Auth:: user(). См. http://laravel.com/docs/4.2/security

Почему, если я могу просто передать текущего пользователя с контроллера?

Предположим, что мы создаем простое приложение для блога с многопользовательской поддержкой. Мы создаем как общедоступный сайт (пользователи могут читать и комментировать записи в блоге), так и административный сайт (пользователи вошли в систему, и у них есть доступ к CRUD к их содержимому в базе данных.)

Здесь "стандартные AR":

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

Теперь, на общедоступном сайте:

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

Это было чисто и просто. Однако на административном сайте требуется нечто большее. Это базовая реализация для всех админ-контроллеров:

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

Таким образом, была добавлена ​​функциональность входа, и текущий пользователь будет сохранен в глобальном. Теперь модель пользователя выглядит следующим образом:

# Let 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 теперь потокобезопасен

Позвольте распространить другие модели, чтобы воспользоваться этим:

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

Модель публикации была расширена, так что она чувствует себя там, только в настоящее время вошедших в сообщения пользователей. Как здорово это!

Администратор почты администратора может выглядеть примерно так:

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

Для модели комментариев версия администратора может выглядеть так:

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

IMHO: Это мощный и безопасный способ убедиться, что пользователи могут получать/изменять только свои данные за один раз. Подумайте, сколько разработчиков нужно написать тестовый код, если каждый запрос должен начинаться с "current_user.posts.whatever_method (...)"? Много.

Исправьте меня, если я ошибаюсь, но я думаю:

Все о разграничении проблем. Даже когда выясняется, что только контроллер должен обрабатывать проверки подлинности, ни в коем случае текущий пользователь должен оставаться на уровне контроллера.

Только помнить: НЕ используйте это! Помните, что могут быть сотрудники электронной почты, которые не используют User.current или вы можете получить доступ к приложению с консоли и т.д.

Ответ 6

Хорошо, я думаю, что current_user - это, наконец, экземпляр пользователя, поэтому не добавляйте эти разрешения в модель User или в модель данных u хотите, чтобы разрешения были применены или запрошены

Я предполагаю, что вам нужно как-то реструктурировать вашу модель и передать текущего пользователя в качестве параметра, например:

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

Ответ 7

У меня есть это в моем приложении. Он просто ищет текущий сеанс контроллеров [: user] и устанавливает для него переменную класса User.current_user. Этот код работает в производстве и довольно прост. Хотелось бы сказать, что я придумал это, но я полагаю, что позаимствовал его у интернет-гения в другом месте.

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
  attr_accessor :current_user
end

Ответ 8

Я всегда удивляюсь ответам "просто не делай этого" людьми, которые ничего не знают о вопросе, лежащем в основе бизнеса. Да, вообще этого следует избегать. Но есть обстоятельства, когда это и целесообразно, и очень полезно. У меня был только один.

Вот мое решение:

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

Это переводит стек назад, ища фрейм, который отвечает на current_user. Если ни один не найден, он возвращает nil. Это можно сделать более надежным, подтвердив ожидаемый тип возвращаемого значения и, возможно, подтвердив, что владелец фрейма является типом контроллера, но обычно работает только денди.

Ответ 9

Я использую плагин Declarative Authorization, и он делает что-то похожее на то, что вы упоминаете с помощью current_user. Он использует before_filter, чтобы вытащить current_user и сохранить его там, где слой модели может получить к нему. Выглядит так:

# 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

Я не использую особенности модели декларативной авторизации. Я все для подхода "Skinny Controller - Fat Model", но я чувствую, что авторизация (а также аутентификация) - это то, что принадлежит уровню контроллера.

Ответ 10

Я чувствую, что текущий пользователь является частью "контекста" вашей модели MVC, думает о текущем пользователе, как о текущем времени, текущем потоке ведения журнала, текущем уровне отладки, текущей транзакции и т.д. Вы можете пройти все эти "модальности" как аргументы в ваши функции. Или вы делаете его доступным переменными в контексте вне текущего тела функции. Локальный контекст нити - лучший выбор, чем глобальные переменные или другие переменные с областью видимости из-за простой безопасности потоков. Как сказал Джош К, опасность, связанная с локализацией потоков, заключается в том, что после выполнения задачи они должны быть очищены, что может сделать для вас инфраструктура инъекций зависимостей. MVC представляет собой несколько упрощенную картину действительности приложения, а не все ее охватывает.