Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails: Access to current_user from within a model in Ruby on Rails

I need to implement fine-grained access control in a Ruby on Rails app. The permissions for individual users are saved in a database table and I thought that it would be best to let the respective resource (i.e. the instance of a model) decide whether a certain user is allowed to read from or write to it. Making this decision in the controller each time certainly wouldn’t be very DRY.
The problem is that in order to do this, the model needs access to the current user, to call something like may_read?(current_user, attribute_name). Models in general do not have access to session data, though.

There are quite some suggestions to save a reference to the current user in the current thread, e.g. in this blog post. This would certainly solve the problem.

Neighboring Google results advised me to save a reference to the current user in the User class though, which I guess was thought up by someone whose application does not have to accommodate a lot of users at once. ;)

Long story short, I get the feeling that my wish to access the current user (i.e. session data) from within a model comes from me doing it wrong.

Can you tell me how I’m wrong?

like image 864
knuton Avatar asked Oct 14 '09 18:10

knuton


4 Answers

I'd say your instincts to keep current_user out of the model are correct.

Like Daniel I'm all for skinny controllers and fat models, but there is also a clear division of responsibilities. The purpose of the controller is to manage the incoming request and session. The model should be able to answer the question "Can user x do y to this object?", but it's nonsensical for it to reference the current_user. What if you are in the console? What if it's a cron job running?

In many cases with the right permissions API in the model, this can be handled with one-line before_filters that apply to several actions. However if things are getting more complex you may want to implement a separate layer (possibly in lib/) that encapsulates the more complex authorization logic to prevent your controller from becoming bloated, and prevent your model from becoming too tightly coupled to the web request/response cycle.

like image 165
gtd Avatar answered Nov 04 '22 04:11

gtd


The Controller should tell the model instance

Working with the database is the model's job. Handling web requests, including knowing the user for the current request, is the controller's job.

Therefore, if a model instance needs to know the current user, a controller should tell it.

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

This assumes that Item has an attr_accessor for current_user.

(Note - I first posted this answer on another question, but I've just noticed that question is a duplicate of this one.)

like image 33
Nathan Long Avatar answered Nov 04 '22 03:11

Nathan Long


Although this question has been answered by many I just wanted to add my two cents in quickly.

Using the #current_user approach on the User model should be implemented with caution due to Thread Safety.

It is fine to use a class/singleton method on User if you remember to use Thread.current as a way or storing and retrieving your values. But it is not as easy as that because you also have to reset Thread.current so the next request does not inherit permissions it shouldn't.

The point I am trying to make is, if you store state in class or singleton variables, remember that you are throwing thread safety out the window.

like image 36
Josh K Avatar answered Nov 04 '22 03:11

Josh K


I'm all in for skinny controller & fat models, and I think auth shouldn't break this principle.

I've been coding with Rails for an year now and I'm coming from PHP community. For me, It's trivial solution to set the current user as "request-long global". This is done by default in some frameworks, for example:

In Yii, you may access the current user by calling Yii::$app->user->identity. See http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html

In Lavavel, you may also do the same thing by calling Auth::user(). See http://laravel.com/docs/4.2/security

Why if I can just pass the current user from controller??

Let's assume that we are creating a simple blog application with multi-user support. We are creating both public site (anon users can read and comment on blog posts) and admin site (users are logged in and they have CRUD access to their content on the database.)

Here's "the standard ARs":

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

Now, on the public site:

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

That was clean & simple. On the admin site however, something more is needed. This is base implementation for all admin controllers:

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

So login functionality was added and the current user gets saved to a global. Now User model looks like this:

# 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 is now thread-safe

Let's extend other models to take advantage of this:

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

Post model was extended so that it feels like there's only currently logged in user's posts. How cool is that!

Admin post controller could look something like this:

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

For Comment model, the admin version could look like this:

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: This is powerful & secure way to make sure that users can access / modify only their data, all in one go. Think about how much developer needs to write test code if every query needs to start with "current_user.posts.whatever_method(...)"? A lot.

Correct me if I'm wrong but I think:

It's all about separation of concerns. Even when it's clear that only controller should handle the auth checks, by no means the currently logged in user should stay in the controller layer.

Only thing to remember: DO NOT overuse it! Remember that there may be email workers that are not using User.current or you maybe accessing the application from a console etc...

like image 15
Hesse Avatar answered Nov 04 '22 02:11

Hesse