Доступ к 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 или вы можете получить доступ к приложению с консоли и т.д.
Ответ 5
Древний поток, но стоит отметить, что начиная с Rails 5.2, есть запутанное решение этой проблемы: текущая модель синглтона, описанная здесь: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything
Ответ 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 представляет собой несколько упрощенную картину действительности приложения, а не все ее охватывает.