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

13

82

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?

Countercurrent answered 14/10, 2009 at 18:45 Comment(2)
Ironically, seven years after this was asked, "doing it wrong" is 404ing. :)Enactment
"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." The model may not be the best place for that logic. Gems like Pundit and Authority establish a pattern of having a separate "policy" or "authorizer" class designated for each model (and models can share them, if you like). These classes provide ways to easily work with the current user.Plowman
F
49

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.

Fineable answered 15/10, 2009 at 19:35 Comment(7)
Thanks, your concerns are convincing. I will have to investigate deeper into how some of the access control plugins for Rails are ticking.Countercurrent
Authorization patterns break MVC by design, any way to go around this usually just ends up worse then simply breaking MVC in that area. If you need to use User.current_user through current thread's registry then do it, just do your best not to overuse itMakeshift
I just think the idea that models should not know what user is accessing them is just an ill-conceived effort at perfection. NOT ONE SINGLE PERSON HERE WOULD ADVISE YOU TO PASS IN THE TIME ZONE to the model in the method params. That would be stupid. But that's the same thing...You have a time object, and most apps have the application controller set the time zone, often based on who is logged in, and then the models know the time zone.Aphra
@Aphra Check your assumptions because most of the time models are best served by plain UTC timestamps, and localizing at the UI level. Sometimes your model does need to know time-zone related things, but only if the model is actually associated to a physical location. Same thing applies to users, maybe the model has the responsibility to determine access, but model it appropriately. To see why current_user on models is a bad idea consider: should it be impossible to manipulate an object in a script or process without injecting a user?Fineable
I don't understand what you are saying. Is your rails environment not such that the model knows the time zone and always converts on the way in and out? And I guess you have a specific example in mind but I don't know what it is. I do think sometimes it would be nice to have the user available to the model. Sometimes it would be abused. But the rule that it can never be available is not consistent with the standard on TZ.Aphra
Concrete example: In the controller a current_user method is appropriate because in the context of a web request it logically makes sense. In the model there is no standard context like a request/response cycle to hang your hat on. It makes more sense to model permissions, so you might have a Model#accessible_by(user) method.Fineable
using policies should make sense here. checkout a gem called pundit. it provides a separate layer (to keep your controller skinny) for authorization.Weighin
P
38

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.)

Plowman answered 30/11, 2012 at 13:57 Comment(3)
You make a principled argument, but practically speaking, this can result in a lot of extra code setting the current user in a lot of places. In my opinion, sometimes the thread-local User.current_user approach makes the rest of the code shorter and easier to understand, despite breaking MVC.Burkholder
amazing! I have several nested objects, so what I added the ids into the views (hidden) and the attr_accessor into the modelCatheryncatheter
@Burkholder Perhaps the real problem, in the OP's case, is having the model handle permission checks. A separate "policy" or "authorizer" class can be handed the model and the current user and authorize from there. This approach means you don't have the model checking permissions in contexts where you don't want them checked, like Rake tasks.Plowman
V
37

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.

Vomit answered 3/11, 2009 at 21:52 Comment(1)
+10 if I could for this remark. Saving request state to any singleton class methods/variables is a Very Bad Idea.Tuberous
S
21

To shed more light on armchairdj's answer

I faced this challenge when working on a Rails 6 application.

Here's how I solved it:

From Rails 5.2 you now we can add a magic Current singleton which acts like a global store accessible from anywhere inside your app.

First, define it in your models:

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

Next, set the user somewhere in your controller to make it accessible in models, jobs, mailers, or wherever you want:

# 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

Now you can call the Current.user in your models:

# 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

OR you can call the Current.user in your forms:

# 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

OR you can call the Current.user in your views:

# 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 %>

Note: You may say: “This feature breaks the Separation of Concerns principle!” Yes, it does. Don’t use it if it feels wrong.

You can read more about this answer here: Current everything

That's all.

I hope this helps

Spit answered 13/8, 2020 at 7:29 Comment(0)
M
15

Ancient thread, but worth noting that starting in Rails 5.2, there's a baked-in solution to this: the Current model singleton, covered here: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything

Meat answered 17/4, 2018 at 20:12 Comment(0)
N
14

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...

Nerve answered 27/1, 2015 at 10:37 Comment(0)
D
9

Well my guess here is that current_user is finally a User instance, so, why don't u add these permissions to the User model or to the data model u want to have the permissions to be applied or queried?

My guess is that u need to restructure your model somehow and pass the current user as a param, like doing:

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
Darb answered 14/10, 2009 at 19:30 Comment(0)
B
9

This is 2021 calling. Since rails 5.2 there's a new global API that can be used, but use it with caution as stated in the API docs:

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

Abstract super class that provides a thread-isolated attributes singleton, which resets automatically before and after each request. This allows you to keep all the per-request attributes easily available to the whole system.

A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result. Current should only be used for a few, top-level globals, like account, user, and request details. The attributes stuck in Current should be used by more or less all actions on all requests. If you start sticking controller-specific attributes in there, you're going to create a mess.

# 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
Bonnybonnyclabber answered 24/9, 2021 at 10:15 Comment(5)
This is an insane amount of boilerplate. Why use the framework at all for this functionality, if I have to write this much code myself anyway?Halie
It's a personal opinion whether this is a lot of boilerplate or not. It's one file with three lines of code (one line inside the class actually) and a few more lines where you actually have to set that variable. All in all, I consider this to be insanely few lines of code. I wonder how many npm packages this functionality would take in Javascript space? :) This is a global, per-request variable. It's crazy how little you have to write to get that functionality.Bonnybonnyclabber
In the end, I just made current_user a parameter on the relevant model function, and passed in it where that function was called.Halie
That's probably the best solution, at least when looking from the "best practices" point of view. 👍Bonnybonnyclabber
Finally got devise-guests working with cancan thanks to this. Just changed Current.user = current_user to Current.user = current_or_guest_userBedizen
B
7

I'm always amazed at "just don't do that" responses by people who know nothing of the questioner's underlying business need. Yes, generally this should be avoided. But there are circumstances where it's both appropriate and highly useful. I just had one myself.

Here was my solution:

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

This walks the stack backwards looking for a frame that responds to current_user. If none is found it returns nil. It could be made more robust by confirming the expected return type, and possibly by confirming owner of the frame is a type of controller, but generally works just dandy.

Blush answered 5/1, 2016 at 2:39 Comment(5)
Really, like when? I can't think of a single reason why anyone would want to do this. Simply don't do it. It's nonsensical, error prone, and will render your code unusable (if the context was is anything other than an HTTP request/response). There are far better solutions, like setting an attr_acessor to the current user from the caller. I'm pretty sure almost every single seasoned programmer will tell you this is a crap idea, and you shouldn't do it. I like the novelty of your response, but it's an abomination.Extensometer
My case was capturing the user (if any) who triggered a financial transaction, no matter how/which code path they got there.Blush
@Mohamad, Would be nice if you could elaborate more about using attr_acessor Tailor
No production code should depend on runtime debug features like this.Reveal
This solution is saving my bacon right now. We have dozens of models that all suddenly need to temporarily know the current_user to provide different API connection details based on the user. I'm using the much lighter bindings gem instead of debug_inspector but basically the same technique. It'll live in production for about a week and then it'll all get ripped out. Thank you very much! (We are on Rails 5.1 right now so the preferred 5.2+ solution of Current is just out of reach.)Displease
N
4

I'm using the Declarative Authorization plugin, and it does something similar to what you are mentioning with current_user It uses a before_filter to pull current_user out and store it where the model layer can get to it. Looks like this:

# 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

I'm not using the model features of Declarative Authorization though. I'm all for the "Skinny Controller - Fat Model" approach, but my feeling is that authorization (as well as authentication) is something that belongs in the controller layer.

Norwood answered 14/10, 2009 at 19:34 Comment(3)
authorization (as well as authentication) belongs to both controller layer (where you permit access to an action) as well as to model (you permit access to a resource there)Gascony
do not use class variables! Use Thread.current['current_user'] or something similar instead. That is threadsafe! Dont forgot to reset the current_user at the end of the request!Inordinate
This may look like a class variable, but it is actually just a method that sets the Thread.current. github.com/stffn/declarative_authorization/blob/master/lib/…Sherasherar
F
4

I have this in an application of mine. It simply looks for the current controllers session[:user] and sets it to a User.current_user class variable. This code works in production and is pretty simple. I wish I could say I came up with it, but I believe I borrowed it from an internet genius elsewhere.

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
Friendly answered 14/10, 2009 at 21:32 Comment(10)
Thanks! If this really works, that would mean that a separate instance of the User class (by which I really mean a class, not an object) is being generated for each request/session. Is that so? You say that you use it in production, so I guess it has to be that way, but it sure appears strange to me.Countercurrent
The above code is actually used in conjunction with automatically setting a created_by and updated_by values of a record on save, based of course on session data. We aren't actually creating multiple instances, we are updating a class variable.Friendly
Why I am confused: You are setting a class variable, User.current_user. I though that that would be a bad idea, as I assumed that the same instance of the User class (as in “Classes in Ruby are first-class objects—each is an instance of class Class.” ruby-doc.org/core/classes/Class.html) would be used throughout a Rails application. If that were the case, this method would obviously lead to unwanted results if several users were active simultaneously. As you are using this in production I assume that it works, but that means that there are several instances of the User class.Countercurrent
It would normally be a bad idea if you were dealing with an application with state. However, this variable is being set on every request in a web stack. Hence, Request -> Set Class Var -> Do Something -> Return Result. The two requests don't bleed into each other, everything starts fresh at the beginning of each request. Out with old, in with the new.Friendly
I just did a google search and found where I think I may have picked this up. In this post he actually goes into threading, and at the end realizes that it wasn't necessary. pluitsolutions.com/2006/08/15/…Friendly
Actually this has a serious flaw in it in production mode that you would never find in development mode. In development the class is reloaded on every request, so the variable would always start out empty. However in production the class stays. jimfish's logic is almost correct, but because of the 'unless c.session[:user].nil?' clause, a non-logged-in user could potentially piggyback on the previous users permissions. If you really want to use this, you should remove that clause.Fineable
Good to know, thanks to both of you! Not sure how I am going to implement it yet (in the model or not).Countercurrent
Very bad idea. Users are now shared across sessions. Security flaw.Gascony
@dasil003 think this simple modification will fix that: User.current_user = c.session[:user].present? ? User.find(c.session[:user]) : nilFinley
you should use an around_filter and add this to the bottom of the block ensure User.current_user = nil also, instead of cattr, try using a Threadsafe approach def self.current_user=(user) Thread.current[:current_user] = user end def self.current_user Thread.current[:current_user] endRichrichara
U
0

My feeling is the current user is part of the "context" of your MVC model, think of the current user like of the current time, the current logging stream, the current debugging level, the current transaction etc. You could pass all these "modalities" as arguments into your functions. Or you make it available by variables in a context outside the current function body. Thread local context is the better choice than global or otherwise scoped variables because of easiest thread safety. As Josh K said, the danger with thread locals is that they must be cleared after the task, something a dependency injection framework can do for you. MVC is a somewhat simplified picture of the application reality and not everything is covered by it.

Ube answered 27/1, 2015 at 9:4 Comment(0)
O
0

I am so very late to this party, but if you need fine-grained access control or have complex permissions I would definitely recommend the Cancancan Gem: https://github.com/CanCanCommunity/cancancan

It allows you to define permissions at every action in your Controller to whatever you want, and since you define the current ability on any controller you can send any parameters you need, like the current_user. You can define a generic current_ability method in ApplicationController and set up things automagically:

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

This way, you can have a UserAbilities Class linked to your UserController, PostAbilities to your PostController and so on. And then define the complex rules in there:

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

It's a great Gem! hope it helps!

Ober answered 6/8, 2020 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.