Rails: доступ до поточного_користувача з моделі в Ruby on Rails


75

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

Є чимало пропозицій, як зберегти посилання на поточного користувача у поточній темі, наприклад, у цій публікації в блозі . Це, безсумнівно, вирішило б проблему.

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

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

Чи можете ви сказати мені, як я помиляюся?


За іронією долі, через сім років після того, як це запитали, "робити це неправильно" - це 404 роки. :)
Ендрю Грімм,

1
"Я вважав, що найкраще дозволити відповідному ресурсу (тобто екземпляру моделі) вирішити, чи дозволено певному користувачеві читати з нього чи писати на нього". Модель може бути не найкращим місцем для цієї логіки. Камені, такі як Pundit та Authority, встановлюють схему окремого класу "політика" або "авторизатор", призначений для кожної моделі (і моделі можуть ними ділитися, якщо хочете). Ці класи надають способи легкої роботи з поточним користувачем.
Натан Лонг

Відповіді:


45

Я б сказав, що ваші інстинкти уникати current_userмоделі є правильними.

Як і Даніель, я все для худих контролерів та товстих моделей, але є також чіткий розподіл обов'язків. Призначення контролера - керувати вхідним запитом та сеансом. Модель повинна мати можливість відповісти на запитання "Чи може користувач x зробити y з цим об'єктом?", Але безглуздо для нього посилання на current_user. Що робити, якщо ви знаходитесь у консолі? Що робити, якщо це запущена робота cron?

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


Дякую, ваші занепокоєння переконливі. Мені доведеться глибше дослідити, як тикають деякі плагіни контролю доступу для Rails.
knuton

4
Шаблони авторизації порушують MVC за задумом, будь-який спосіб обійти це, як правило, закінчується гірше, ніж просто порушуючи MVC у цій області. Якщо вам потрібно скористатися User.current_userпоточним реєстром потоків, зробіть це, просто зробіть все можливе, щоб не зловживати ним
bbozo

1
Я просто думаю, що ідея про те, що моделі не повинні знати, до кого користувач звертається до них, - це просто непродумана спроба досконалості. ЖОДНА ОДИНА ОСОБА ТУТ НЕ РАДИТИ ВАМ, ЩОБ ПРИЙТИ В ЧАСОВУ ЗОНУ до моделі в параметрах методу. Це було б глупо. Але це одне і те ж ... У вас є часовий об’єкт, і в більшості програм контролер додатків встановлює часовий пояс, часто на основі того, хто ввійшов у систему, і тоді моделі знають часовий пояс.
nroose 01.03.17

@nroose Перевірте свої припущення, оскільки більшість часових моделей найкраще обслуговуються простими часовими позначками UTC та локалізацією на рівні інтерфейсу користувача. Іноді ваша модель дійсно потрібно знати часових поясів , пов'язані речі, але тільки якщо модель насправді пов'язаний з фізичним місцезнаходженням. Те саме стосується користувачів, можливо, модель несе відповідальність визначати доступ, але змоделювати його належним чином. Щоб зрозуміти, чому current_user на моделях - погана ідея, подумайте: чи неможливо маніпулювати об’єктом у сценарії чи процесі, не вводячи користувача?
gtd

1
використання політик має мати тут сенс. замовити дорогоцінний камінь під назвою pundit. він надає окремий шар (щоб ваш контролер залишався худим) для авторизації.
Нірав Ганді,

35

Хоча на це питання відповіли багато людей, я просто хотів швидко додати свої два центи.

Використання підходу #current_user на моделі користувача слід реалізовувати з обережністю через безпеку потоків.

Добре використовувати метод User / singleton для User, якщо ви пам’ятаєте використовувати Thread.current як спосіб або зберігати та отримувати свої значення. Але це не так просто, тому що вам також доведеться скинути Thread.current, тому наступний запит не успадковує дозволи, яких не повинен.

Я намагаюся сказати, що якщо ви зберігаєте стан у змінних класу чи однотонні, пам’ятайте, що викидаєте безпеку потоків у вікно.


14
+10, якби міг за це зауваження. Збереження стану запиту до будь-яких методів / змінних класу singleton є дуже поганою ідеєю.
molf

33

Контролер повинен повідомити примірник моделі

Робота з базою даних - це робота моделі. Обробка веб-запитів, включаючи знання користувача про поточний запит, є роботою контролера.

Отже, якщо екземпляр моделі повинен знати поточного користувача, контролер повинен це повідомити.

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

Це передбачає, що Itemмає attr_accessorдля current_user.

(Примітка - Я вперше розмістив цю відповідь на інше питання, але я щойно помітив, що це питання є дублікатом цього.)


3
Ви наводите принциповий аргумент, але на практиці це може призвести до великої кількості додаткового коду, який встановлює поточного користувача у багатьох місцях. На мою думку, іноді локально-потоковий User.current_userпідхід робить решту коду коротшим і легшим для розуміння, незважаючи на порушення MVC.
antinome

дивовижний! У мене є кілька вкладених об'єктів, тому те, що я додав ідентифікатори у подання (приховані) та attr_accessor у модель
Cris R

1
@antinome Можливо, справжньою проблемою, у випадку OP, є перевірка дозволів на обробку моделі. Окремий клас "політики" або "авторизатора" може бути переданий моделі та поточному користувачеві та авторизований звідти. Цей підхід означає, що у вас немає дозволів на перевірку моделі в контекстах, де ви не хочете, щоб вони перевірялися, як завдання Rake.
Натан Лонг

14

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

Я кодую 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-доступ до свого вмісту в базі даних).

Ось "стандартні АР":

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

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

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, або ви, можливо, отримуєте доступ до програми з консолі тощо ...



8

Ну, я здогадуюсь, що 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

7

Щоб пролити більше світла на відповідь armchairdj

Я зіткнувся з цією проблемою, працюючи над додатком Rails 6 .

Ось як я це вирішив :

Тепер із Rails 5.2 ми можемо додати магічний 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

Тепер ви можете зателефонувати за 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 %>

Примітка : Ви можете сказати: "Ця функція порушує принцип розділення турбот!" Так. Не використовуйте його, якщо він відчуває себе неправильно.

Детальніше про цю відповідь ви можете прочитати тут: Актуально все

Це все.

Сподіваюся, це допоможе


5

Мене завжди дивують відповіді "просто не роби цього" людей, які нічого не знають про необхідність бізнесу допитувача. Так, загалом цього слід уникати. Але існують обставини, коли це одночасно і доречно, і дуже корисно. У мене щойно була одна.

Ось моє рішення:

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. Якщо жодного не знайдено, він повертає нуль. Це можна зробити більш надійним, підтвердивши очікуваний тип повернення, і, можливо, підтвердивши, що власник кадру є типом контролера, але, як правило, працює просто чудово.


1
Справді, як коли? Я не можу придумати жодної причини, чому хтось хотів би це зробити. Просто не робіть цього. Він безглуздий, схильний до помилок і зробить ваш код непридатним (якщо контекст був іншим, ніж HTTP-запит / відповідь). Є набагато кращі рішення, наприклад, встановити attr_acessorпоточного користувача від абонента. Я майже впевнений, що майже кожен досвідчений програміст скаже вам, що це дурна ідея, і ви не повинні цього робити. Мені подобається новизна вашої відповіді, але це огида.
Мохамад

У моєму випадку було схоплено користувача (якщо він є), який ініціював фінансову операцію, незалежно від того, як / який шлях коду вони там отримали.
keredson

2
@Mohamad, Було б непогано, якби ви могли детальніше розповісти про використанняattr_acessor
WM

Жоден робочий код не повинен залежати від таких функцій налагодження під час виконання.
Shyam Habarakada

5

У мене це є в моєму додатку. Він просто шукає поточний сеанс контролерів [: 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

1
Чому я розгублений: Ви встановлюєте змінну класу User.current_user. Я вважаю, що це було б поганою ідеєю, оскільки я припустив, що той самий екземпляр класу User (як у "Класи в Ruby - це першокласні об'єкти - кожен є екземпляром класу Class.") Ruby-doc.org/core /classes/Class.html ) буде використовуватися у всій програмі Rails. Якби це було так, цей метод, очевидно, призвів би до небажаних результатів, якщо одночасно діяли кілька користувачів. Оскільки ви використовуєте це у виробництві, я припускаю, що воно працює, але це означає, що існує кілька екземплярів класу User.
knuton

5
Насправді це має серйозний недолік у виробничому режимі, якого ви ніколи не знайдете в режимі розробки. У розробці клас перезавантажується при кожному запиті, тому змінна завжди починається порожньою. Однак на виробництві клас залишається. Логіка jimfish майже правильна, але через "якщо c.session [: user] .nil?" Застереження, що не зареєстрований користувач може потенційно повернутися до дозволів попередніх користувачів. Якщо ви дійсно хочете цим скористатися, слід видалити це застереження.
gtd

1
Дуже погана ідея. Тепер користувачі розподіляються між сесіями. Недолік безпеки.
lzap

4
@ dasil003 вважає, що ця проста модифікація виправить: User.current_user = c.session [: user] .present? ? User.find (c.session [: user]): nil
tybro0103

1
ви повинні використовувати around_filterі додати це внизу блоку ensure User.current_user = nil, а також замість cattr, спробуйте використовувати підхід def self.current_user=(user) Thread.current[:current_user] = user end def self.current_user Thread.current[:current_user] end
Threadsafe

4

Я використовую плагін Declarative Authorization, і він робить щось подібне до того, про що ви згадуєте, current_userвикористовуючи a, 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

Однак я не використовую модельні особливості декларативної авторизації. Я все за підхід "Худий контролер - жирна модель", але я відчуваю, що авторизація (як і автентифікація) - це те, що належить до рівня контролера.


авторизація (як і автентифікація) належить як рівню контролера (де ви дозволяєте доступ до дії), так і моделі (ви дозволяєте доступ до ресурсу там)
lzap

2
не використовуйте змінні класу! Замість цього використовуйте Thread.current ['current_user'] або щось подібне. Це безпечно! Не забудьте скинути поточний_користувач в кінці запиту!
рето

1
Це може виглядати як змінна класу, але насправді це просто метод, який встановлює Thread.current. github.com/stffn/declarative_authorization/blob/master/lib/…
evanbikes

0

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


0

Я дуже запізнився з цією вечіркою, але якщо вам потрібен чіткий контроль доступу або ви маєте складні дозволи, я точно рекомендую 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

Це чудовий самоцвіт! сподіваюся, це допоможе!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.