How to add a new column to Rails' sessions
Posted by Luke Ludwig Mon, 26 Nov 2007 23:59:00 GMT
For a rails app at work we store user access privileges in the session. This is done as an optimization to avoid an extra sql query that would need to be done for every page view to determine if the user has edit privileges. Security-minded people may see this as a security hole. For this application I don't see this as a big deal. Its not like our rails app is controlling the launch of nuclear missiles.
The problem is that when an admin goes to modify the user access privileges for someone, the changes won't take affect until the user next logs in since the user access privileges are stored in the session. So if the user is already logged in this is a problem. They will have to log out and log back in for the changes to take affect. The solution to this problem is to modify the session of the user who's access privileges were modified.
To do this we need to add a user_id column to the sessions table. This can be done like any other migration. The tricky part is accessing the user_id column. We will want to set the user_id of the session when someone logs in. This of course will not work:
session.user_id = @user.id
The session we are so familiar with is not like other ActiveRecord objects. If you are using ActiveRecordStore for your session storage (the rails built-in database session storage mechanism), this is easy. For ActiveRecordStore, the session has a model attribute allowing you to access the ActiveRecord session object. First you need to make a session model class which extends ActiveRecord. Mine looked lke this:
class Session < ActiveRecord::Base
end
Then inside your "login" action you can do:
session.model.user_id = @user.id
Then just rely on the automatic session saving built into rails to save the user_id to the database.
If you are using SqlSessionStore, you don't have access to the model attribute. Instead you can do this to set the user_id:
my_session = Session.find_by_session_id(session.session_id)
my_session.update_attribute(:user_id, current_user.id)
Now presumably at some point in your application you will need to access and make use of the user_id column. For me this is when the user access privileges are modified. Since we created a session.rb model class, we can use the built in rails find method to find all sessions for the user.
sessions = Session.find(:all, :conditions => "user_id = #{user.id}", :select => "session_id")
To take advantage of the built-in session data marshaling in rails, we need to access the session like this:
user_session = CGI::Session::ActiveRecordStore::Session.find_by_session_id(sess.session_id)
Then we can set a session value using the data attribute and save the session off. Here is the full code for updating the sessions:
sessions.each do |sess|
user_session = CGI::Session::ActiveRecordStore::Session.find_by_session_id(sess.session_id)
user_session.data[:access_privileges] = new_access_privileges
user_session.save
end
This is great, exactly the problem we're trying to solve. There only seems to be one problem, if you change something and your own session needs to change, it doesn't seem to work. It will actually change the db, I've seen it do it by using the console during a sleep, but the first access to session changes it back. We haven't been able to track down what's causing this. Where it's being cached. Any suggestions?
If I am following you correctly, you are saying that if the current session for the request is one of the sessions that gets updated in the sessions.each loop above it doesn't seem to affect it. At the end of every request that rails processes it saves the current session to the database, which overwrites the change that was just made to it. This also explains why you were able to see the change take affect in the console, but then with the next request the change was gone. So if you need this code to potentially affect the current request's session as well you could do this:
sessions.each do |sess|
if sess.session_id == session.session_id
session.data[:access_privileges] = new_access_privileges
else
user_session = CGI::Session::ActiveRecordStore::Session.find_by_session_id(sess.session_id)
user_session.data[:access_privileges] =new_access_privileges
user_session.save
end
end
Notice that you don't need to call save on session.save since rails does this for you at the end of the request.
That's exactly what we found. We've been trying to find another way around it, since we originally had the session updater in a library file without access to the session. Thanks for the response!