web-dev-qa-db-ja.com

Ruby on Railsのモデル内からcurrent_userへのアクセス

Ruby on Rails app。で個々のユーザーのパーミッションはデータベーステーブルに保存されていると思いました。特定のユーザーに読み取りまたは書き込みを許可するかどうかをそれぞれのリソース(モデルのインスタンス)に決定させるのが最善です。この決定をコントローラーで毎回行うことは確かにそれほど乾燥しません。
問題は、これを行うには、モデルが現在のユーザーにアクセスして、may_read?(current_userattribute_name)</ code>。ただし、一般的にモデルはセッションデータにアクセスできません。

現在のスレッドに現在のユーザーへの参照を保存するためのいくつかの提案があります。 in このブログ投稿 。これは確かに問題を解決します。

近隣のGoogleの結果から、Userクラスに現在のユーザーへの参照を保存するように勧められました。これは、アプリケーションが一度に多くのユーザーに対応する必要のない人が考えたものと思われます。 ;)

簡単に言えば、モデル内から現在のユーザー(つまりセッションデータ)にアクセスしたいのは、私が 間違ったことをする から来るという感じがします。

どうして間違っているのか教えてもらえますか?

68
knuton

current_userをモデルに入れないというあなたの本能は正しいと思います。

ダニエルのように、私はすべてスキニーコントローラーとファットモデルを担当していますが、責任の明確な区分もあります。コントローラーの目的は、着信要求とセッションを管理することです。モデルは「ユーザーxはこのオブジェクトに対してyを実行できますか?」という質問に答えられるはずですが、current_userを参照するのは無意味です。コンソールにいる場合はどうですか? cronジョブが実行されている場合はどうなりますか?

多くの場合、モデルに適切な権限APIがあれば、これはいくつかのアクションに適用されるbefore_filtersの1行で処理できます。ただし、状況がさらに複雑になっている場合は、コントローラーが肥大化するのを防ぎ、モデルが過度に結合されないようにするために、より複雑な承認ロジックをカプセル化する別のレイヤー(おそらくlib/)を実装することができますWeb要求/応答サイクル。

42
gtd

多くの人がこの質問に答えていますが、2セントをすぐに追加したかっただけです。

ユーザーモデルで#current_userアプローチを使用する場合は、スレッドセーフのため、注意して実装する必要があります。

方法としてThread.currentを使用したり、値を保存および取得したりすることを忘れない場合は、Userでクラス/シングルトンメソッドを使用しても構いません。ただし、Thread.currentをリセットする必要があるため、次のリクエストは許可すべきでないパーミッションを継承しないため、それほど簡単ではありません。

私がやろうとしているのは、クラスまたはシングルトン変数に状態を保存する場合、スレッドセーフをウィンドウの外に投げていることを思い出してください。

36
Josh K

コントローラーはモデルインスタンスに通知する必要があります

データベースの操作は、モデルの仕事です。現在のリクエストのユーザーを知ることを含む、Webリクエストの処理は、コントローラーの仕事です。

したがって、モデルインスタンスが現在のユーザーを知る必要がある場合、コントローラーはそれを伝える必要があります。

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

これは、Itemattr_accessor ために current_user

(注-最初にこの回答を 別の質問 に投稿しましたが、質問がこの質問の複製であることに気づきました。)

29
Nathan Long

私はすべてスキニーコントローラーとファットモデルに専念しており、authはこの原則を破るべきではないと思います。

私はRailsで1年間コーディングしてきました。私はPHPコミュニティから来ています。私にとって、現在のユーザーを「request-long global」として設定するのは簡単な解決策です。これは、いくつかのフレームワークでデフォルトで行われます。

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

コメントモデルの場合、管理者バージョンは次のようになります。

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.whatever_method(...)」で開始する必要がある場合に、開発者がテストコードを記述する必要がある量を考えますか?たくさん。

私が間違っているが、私が思う場合は私を修正してください:

問題の分離がすべてです。コントローラーのみが認証チェックを処理する必要があることが明らかな場合でも、現在ログインしているユーザーがコントローラーレイヤーに留まることは決してありません。

覚えておく必要があるのは、使いすぎないことです! User.currentを使用していないメールワーカーがいるか、コンソールなどからアプリケーションにアクセスしている可能性があることに注意してください。

13
Hesse

古代のスレッドですが、Rails 5.2で始まることに注意する価値があります。これには、解決策があります:現在のモデルシングルトン、ここで説明します: https://evilmartians.com/ chronicles/Rails-5-2-active-storage-and-beyond#current-everything

9
armchairdj

私の推測では、current_userは最終的に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
8
khelll

私は、質問者の根底にあるビジネスニーズを何も知らない人々による「それをしないでください」という応答にいつも驚いています。はい、一般的にこれは避けるべきです。しかし、それが適切で非常に有用な状況があります。私は自分で持っていました。

ここに私の解決策がありました:

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を返します。期待される戻り値の型を確認することにより、そしておそらくフレームの所有者がコントローラの型であることを確認することにより、より堅牢にすることができますが、通常はうまく動作します。

5
keredson

私は私のアプリケーションでこれを持っています。単に現在のコントローラーのsession [: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
5
jimfish

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」アプローチに賛成ですが、私の考えでは、認可(および認証)はコントローラー層に属するものだと感じています。

4

私の感じでは、現在のユーザーはMVCモデルの「コンテキスト」の一部であり、現在の時間、現在のロギングストリーム、現在のデバッグレベル、現在のトランザクションなど、現在のユーザーを考えます。関数への引数としての「モダリティ」。または、現在の関数本体の外部のコンテキストの変数で使用できるようにします。スレッドのローカルコンテキストは、最も簡単なスレッドセーフのため、グローバル変数またはその他のスコープ変数よりも優れた選択肢です。 Josh Kが言ったように、スレッドローカルの危険性は、タスクの後にクリアする必要があるということです。 MVCはアプリケーションの現実をいくぶん簡略化した図であり、すべてがそれでカバーされるわけではありません。

0
Markus