45

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.

Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
LearnRails
  • 501
  • 1
  • 5
  • 5
  • 5
    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 it – stephenmurdoch Mar 25 '10 at 06:00
  • 1
    @stephenmurdoch - You have no idea what a sigh that little piece of information just elicited... :) – sscirrus Jun 27 '11 at 21:29
  • possible duplicate of [Access to current\_user from within a model in Ruby on Rails](http://stackoverflow.com/questions/1568218/access-to-current-user-from-within-a-model-in-ruby-on-rails) – Nathan Long Nov 30 '12 at 13:54
  • this kind of situation can be handled using https://github.com/steveklabnik/request_store#requeststore-- gem. – Arup Rakshit Jan 24 '16 at 11:08

7 Answers7

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
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • 2
    Note that this solution is not threadsafe, so be careful with JRuby deployments etc. – molf Mar 30 '10 at 06:20
  • 2
    I've seen over the web, like http://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?) – dmonopoly Jun 20 '12 at 15:00
  • 2
    @daze, the solution as it is now is thread-safe. In its earlier version `Thread.local` was not used, hence the comment from `molf`. – Harish Shetty Jun 20 '12 at 16:22
  • 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? – dmonopoly Jun 20 '12 at 16:31
  • 2
    @daze, it is supposed to be `Thread.current`, I have updated the answer. – Harish Shetty Jun 20 '12 at 16:41
  • 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." :) – Nathan Long Oct 03 '12 at 18:33
  • @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. – Harish Shetty Oct 03 '12 at 21:33
  • @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? – Nathan Long Oct 03 '12 at 23:39
  • `User.current_user = -> { current_user }` in the controller and `Thread.current[:current_user].try :call` in the User model avoids unnecessary db querys – Kaworu Apr 01 '14 at 09:25
  • 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 ! – Phantomwhale Jun 22 '17 at 06:32
  • 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. – AFOC May 04 '18 at 00:16
  • @AFOC 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. – Harish Shetty May 04 '18 at 04:16
  • @HarishShetty I see, it's necessary for that type of callback. I am using `before_action` for one specific action in one controller only. – AFOC May 07 '18 at 17:04
  • 3
    @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. – Nathan Long Mar 05 '19 at 21:58
  • 1
    @NathanLong No offence was taken. I do think using thread local variables is quite tricky so its all good. – Harish Shetty May 04 '19 at 01:50
  • So if the answer for Rails 5 as it is now, threadsafe? – Steven Aguilar Feb 01 '22 at 22:19
  • the answer is thread safe – Harish Shetty Feb 08 '22 at 01:34
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.

Nathan Long
  • 122,748
  • 97
  • 336
  • 451
8

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

murb
  • 1,736
  • 21
  • 31
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
bjb568
  • 11,089
  • 11
  • 50
  • 71
Faisal
  • 19,358
  • 4
  • 30
  • 33
  • 1
    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. – timpone Aug 14 '12 at 21:32
  • 1
    What if the Model does not directly `belong_to :user` because you are working with some nested model? – Matthias Mar 26 '16 at 10:50
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 
John Moore
  • 178
  • 2
  • 6
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

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

keredson
  • 3,019
  • 1
  • 17
  • 20
  • 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". – keredson May 02 '17 at 19:19
  • 2
    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. – MIdhun Krishna Aug 15 '17 at 14:19
  • 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? – keredson Aug 15 '17 at 19:47
  • 2
    Exactly. Think about it. – MIdhun Krishna Aug 24 '17 at 19:09