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

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

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

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

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

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

13 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы пролить больше света на ответ armchairdj

Я столкнулся с этой проблемой при работе над приложением на Rails 6.

Вот как я это решил:

Из Rails 5.2 вы теперь можете добавить магию Current singleton, который действует как глобальный магазин, доступный из любого места внутри вашего приложения.

Сначала определите его в своих моделях:

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

Затем установите пользователя где-нибудь в вашем контроллере, чтобы сделать его доступным в моделях, заданиях, почтовых программах или где угодно:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

Теперь вы можете позвонить в Current.userв ваших моделях:

# app/models/post.rb
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

ИЛИ вы можете позвонить Current.userв ваших формах:

# app/forms/application_registration.rb
class ApplicationRegistration
  include ActiveModel::Model

  attr_accessor :email, :user_id, :first_name, :last_name, :phone,
                    
  def save
    ActiveRecord::Base.transaction do
      return false unless valid?

      # User.create!(email: email)
      PersonalInfo.create!(user_id: Current.user.id, first_name: first_name,
                          last_name: last_name, phone: phone)

      true
    end
  end
end

ИЛИ вы можете позвонить Current.userв ваших представлениях:

# app/views/application_registrations/_form.html.erb
<%= form_for @application_registration do |form| %>
  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email, value: Current.user.email %>
  </div>

  <div class="field">
    <%= form.label :first_name %>
    <%= form.text_field :first_name, value: Current.user.personal_info.first_name %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Примечание. Вы можете сказать: "Эта функция нарушает принцип разделения проблем!" Да, это так. Не используйте его, если что-то не так.

Вы можете прочитать больше об этом ответе здесь: Текущее все

Вот и все.

надеюсь, это поможет

Древний поток, но стоит отметить, что начиная с Rails 5.2, есть запутанное решение для этого: синглтон текущей модели, описанный здесь: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond

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

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

В Yii вы можете получить доступ к текущему пользователю, вызвав Yii::$app->user->identity. См. 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'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 теперь потокобезопасен

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

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

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

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

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

Поправь меня если я не прав но думаю:

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

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

Ну, мое предположение вот что current_user наконец, экземпляр пользователя, поэтому, почему бы вам не добавить эти разрешения в User модель или модель данных вы хотите иметь разрешения, которые будут применяться или запрашиваться?

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

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

Это вызов 2021 года. Начиная с rails 5.2 есть новый глобальный API, который можно использовать, но используйте его с осторожностью, как указано в документации API:

https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

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

Предупреждение: легко переборщить с глобальным синглтоном, таким как Current, и в результате запутать вашу модель. Текущий следует использовать только для нескольких глобальных переменных верхнего уровня, таких как сведения об учетной записи, пользователе и запросе. Атрибуты, застрявшие в Current, должны использоваться более или менее всеми действиями по всем запросам. Если вы начнете наклеивать туда атрибуты, специфичные для контроллера, вы создадите беспорядок.

      # app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# and now in your model
# app/models/post.rb
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

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

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

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, Если ничего не найдено, возвращается ноль. Это можно сделать более надежным, подтвердив ожидаемый тип возврата, и, возможно, подтвердив, что владелец фрейма является типом контроллера, но обычно работает просто модно.

У меня есть это в моем приложении. Он просто ищет текущий сеанс контроллеров [: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
  cattr_accessor :current_user
end

Я использую плагин декларативной авторизации, и он делает нечто похожее на то, что вы упоминаете с 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", но я чувствую, что авторизация (а также аутентификация) принадлежит к уровню контроллера.

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

Я очень опаздываю на эту вечеринку, но если вам нужен детальный контроль доступа или сложные разрешения, я определенно рекомендую Cancancan Gem:https://github.com/CanCanCommunity/cancancan

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

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
  def current_ability
    klass = Object.const_defined?('MODEL_CLASS_NAME') ? MODEL_CLASS_NAME : controller_name.classify
    @current_ability ||= "#{klass.to_s}Abilities".constantize.new(current_user, request)
  end
end

Таким образом, вы можете UserAbilitiesКласс, связанный с вашим UserController, PostAbilities с вашим PostController и так далее. А затем определите там сложные правила:

class UserAbilities
  include CanCan::Ability

  def initialize(user, request)
    if user
      if user.admin?
        can :manage, User
      else
        # Allow operations for logged in users.
        can :show, User, id: user.id
        can :index, User if user
      end
    end
  end
end

Это отличный самоцвет! Надеюсь, это поможет!

Другие вопросы по тегам