Access current_user in model
Asked Answered
M

7

46

I have 3 tables

items (columns are:  name , type)
history(columns are: date, username, item_id)
user(username, password)

When a user say "ABC" logs in and creates a new item, a history record gets created with the following after_create filter. How to assign this username ‘ABC’ to the username field in history table through this filter.

class Item < ActiveRecord::Base
  has_many :histories
  after_create :update_history
  def update_history
    histories.create(:date=>Time.now, username=> ?) 
  end
end

My login method in session_controller

def login
  if request.post?
    user=User.authenticate(params[:username])
    if user
      session[:user_id] =user.id
      redirect_to( :action=>'home')
      flash[:message] = "Successfully logged in "
    else
      flash[:notice] = "Incorrect user/password combination"
      redirect_to(:action=>"login")
    end
  end
end

I am not using any authentication plugin. I would appreciate if someone could tell me how to achieve this without using plugin(like userstamp etc.) if possible.

Masterson answered 25/3, 2010 at 5:57 Comment(4)
your code is not formatted properly so it's a little difficult to read. if you click the 'edit' button, then select your code and press 'ctrl+K' it will automagically indent itTricolor
@Tricolor - You have no idea what a sigh that little piece of information just elicited... :)Whiffle
possible duplicate of Access to current_user from within a model in Ruby on RailsFlexure
this kind of situation can be handled using github.com/steveklabnik/request_store#requeststore-- gem.Nod
N
89

Rails 5

Declare a module

module Current
  thread_mattr_accessor :user
end

Assign the current user

class ApplicationController < ActionController::Base
  around_action :set_current_user
  def set_current_user
    Current.user = current_user
    yield
  ensure
    # to address the thread variable leak issues in Puma/Thin webserver
    Current.user = nil
  end             
end

Now you can refer to the current user as Current.user

Documentation about thread_mattr_accessor

Rails 3,4

It is not a common practice to access the current_user within a model. That being said, here is a solution:

class User < ActiveRecord::Base
  def self.current
    Thread.current[:current_user]
  end

  def self.current=(usr)
    Thread.current[:current_user] = usr
  end
end

Set the current_user attribute in a around_filter of ApplicationController.

class ApplicationController < ActionController::Base
  around_filter :set_current_user

  def set_current_user
    User.current = User.find_by_id(session[:user_id])
    yield
  ensure
    # to address the thread variable leak issues in Puma/Thin webserver
    User.current = nil
  end             
end

Set the current_user after successful authentication:

def login
  if User.current=User.authenticate(params[:username], params[:password])
    session[:user_id] = User.current.id
    flash[:message] = "Successfully logged in "
    redirect_to( :action=>'home')
  else
    flash[:notice] = "Incorrect user/password combination"
    redirect_to(:action=>"login")
  end
end

Finally, refer to the current_user in update_history of Item.

class Item < ActiveRecord::Base
  has_many :histories
  after_create :update_history
  def update_history
    histories.create(:date=>Time.now, :username=> User.current.username) 
  end
end
Nucleolar answered 25/3, 2010 at 6:20 Comment(17)
Note that this solution is not threadsafe, so be careful with JRuby deployments etc.Placket
I've seen over the web, like rails-bestpractices.com/posts/47-fetch-current-user-in-models too, that this technique is not thread-safe, but what does that really mean? For a typical Rails app that uses this strategy, is this dangerous? What could go wrong? (After all, doesn't Rails already use Thread to allow time_zone access in the model?)Duprey
@daze, the solution as it is now is thread-safe. In its earlier version Thread.local was not used, hence the comment from molf.Nucleolar
Thanks. However... Thread does not have a method "local" when I try it out in Rails 3.2. Also, I tried out Thread.current, which is what I've seen elsewhere (and it works). Is Thread.current the dangerous one?Duprey
@daze, it is supposed to be Thread.current, I have updated the answer.Nucleolar
This answer is simultaneously impressive and terrifying. I would not have figured it out, but it also goes against the Rails design. I would classify it under the meme of "there, I fixed it." :)Flexure
@NathanLong, At the beginning of the answer, I have discouraged the user from accessing the current user in a model. Over the years I have adopted the "I wouldn't do that, but since you have asked here is how to do it" approach to answering SO questions. That being said, thread local variables are not widely discussed in the Rails community. As long as one understands the life-cycle of these variables, they are quite safe to use.Nucleolar
@HarishShetty - point taken. I wasn't familiar with thread-local variables, so thanks for showing me a new concept. I've also suggested a different method below. Would you take a look?Flexure
User.current_user = -> { current_user } in the controller and Thread.current[:current_user].try :call in the User model avoids unnecessary db querysGonophore
Ah, handy, was getting the Puma thread issue and wondered if there was an talk of needing to explicitly unset the thread local variable I love that this 7 year old answer has been updated as the versions of Rails have progressed - although I think around_filter is long since deprecated for around_action now !Twist
Can anyone explain the purposes of the yield ensure Current.user = nil parts in this? When I put yield, it raised an exception for having no block to follow it. When I put the other section, it made the whole thing never work because it defines what I want to actually be the current user as nil. If I omit those parts it works fine. Not sure about thread-safeness though, would appreciate a link to some detailed explanations of what's going on here.Cotten
@Cotten you have to use yield as the set_current_user is called in an around_filer. Make sure that you are doing the same, otherwise you will get the no block error.Nucleolar
@HarishShetty I see, it's necessary for that type of callback. I am using before_action for one specific action in one controller only.Cotten
@HarishShetty This was long ago, but I just want to say that my comment about your answer was rude, and your solution was reasonable. I'm sorry.Flexure
@NathanLong No offence was taken. I do think using thread local variables is quite tricky so its all good.Nucleolar
So if the answer for Rails 5 as it is now, threadsafe?Tart
the answer is thread safeNucleolar
F
53

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.

Flexure answered 3/10, 2012 at 17:31 Comment(2)
I have taken similar approach in my projects to ensure model layer is separated from the controller layer.Nucleolar
This approach is not always possible, especially when you're using Rails callbacks e.g. after_save.Meridional
V
9

The Rails 5.2 approach for having global access to the user and other attributes is CurrentAttributes.

Varityper answered 23/8, 2018 at 7:29 Comment(0)
D
6

If the user creates an item, shouldn't the item have a belongs_to :user clause? This would allow you in your after_update to do

History.create :username => self.user.username
Dyscrasia answered 25/3, 2010 at 14:21 Comment(2)
I kinda disagree with this point. What about logging information? You want the current_user.id without having to pass user_information around. current_user is analogous to config for an app.Rhetorical
What if the Model does not directly belong_to :user because you are working with some nested model?Kalie
P
1

You could write an around_filter in ApplicationController

around_filter :apply_scope 

def apply_scope 

  Document.where(:user_id => current_user.id).scoping do 
  yield 

end 
Procreate answered 7/6, 2013 at 12:52 Comment(0)
I
1

This can be done easily in few steps by implementing Thread.

Step 1:

class User < ApplicationRecord

  def self.current
    Thread.current[:user]
  end

  def self.current=(user)
    Thread.current[:user] = user
  end

end

Step 2:

class ApplicationController < ActionController::Base
  before_filter :set_current_user

  def set_current_user
    User.current = current_user
  end
end

Now you can easily get current user as User.current

Introspection answered 3/7, 2018 at 11:40 Comment(0)
B
-1

The Thread trick isn't threadsafe, ironically.

My solution was to walk the stack backwards looking for a frame that responds to current_user. If none is found it returns nil. Example:

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

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

Bub answered 5/1, 2016 at 2:46 Comment(4)
Two down-votes in one day, w00t! It has been amazing watching the up/down votes on this response the past 1.5 years, between the "it's a solution to the question" and "the questioner shouldn't be wanting to do that" camps. I still stand by "while you prob. don't want to make it common practice, when you need it you really do need it".Bub
I am in Minsk Russia. I need to go to New York. According to your answer, I should get in a Soyuz space craft, get into ISS, wait for SpaceX rocket, get down, and walk to New York.Chilblain
i don't follow the analogy. is the space the stack? you realize the spaceX rocket doesn't carry people, nor takes anything from space back to earth, right?Bub
Exactly. Think about it.Chilblain

© 2022 - 2024 — McMap. All rights reserved.