Skip to content

Instantly share code, notes, and snippets.

@StephenRoos
Last active March 1, 2017 19:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StephenRoos/a07aef7c306853af7e5f4a7a34d206ab to your computer and use it in GitHub Desktop.
Save StephenRoos/a07aef7c306853af7e5f4a7a34d206ab to your computer and use it in GitHub Desktop.

Overview

Our app relies entirely on the current_user helper method (found in our ApplicationController) to retrieve the User object associated with the currently logged-in user (if any). You guys are using the devise gem for authentication, but you still have a current_user helper method which is made available to ApplicationController (and all subclasses).

Normally, the current_user method looks something like this:

def current_user
  # do some magic to get the user id from the (encrypted) session cookie…
  user_id = get_user_id_from_session

  # load and return the associated User model (if applicable)…
  User.find user_id unless user_id.nil?
end

We change this a bit by first adding some helper methods to our ApplicationController (I'll go into implementation details later):

  • true_user - This method returns the logged-in User ignoring impersonation. This does what the above current_user implementation does.
  • impersonated_user – This method returns the User representing the user being impersonated. Returns nil if no one is being impersonated.
  • impersonate_user!(user) – This method starts impersonating the given user.
  • stop_impersonating_user! – This method stops impersonation.
  • impersonating_user? – Returns true if, and only if, the logged-in user is currently impersonating another user.

We then change the current_user method from the pseudo-code above to something like:

def current_user
  @current_user ||= impersonated_user || true_user
end

If a user is currently being impersonated, current_user will return that User (object). If not, current_user will act how it normally does.

Implementation

Take a look at the files below for some implementation details. For now, ignore the methods marked FEATURE X, these methods are used in the implementation of additional features which I'll describe a bit later.

You'll notice that we keep track of the impersonated user by writing the user ID to the session (which is the same way we keep track of the logged-in user). To make it all work, we added an action in our Admin::UsersController that calls impersonate_user! with the given user ID, and then redirects the user to the root URL (where they’ll start seeing the site as the impersonated user). A button in our Admin interface hits this action (with the chosen user id passed as a parameter), and voila, the impersonation begins.

Additional Features

The pieces I've outlined above should be all that you need to get it working. In our implementation, there are a couple additional pieces of code that we've added to achieve some additional functionality:

  1. Impersonation should be ignored in the Admin area of the site. This means an administrator can continue to perform tasks in our Admin console (as him/herself) while impersonating another user. We accomplish this with the update marked FEATURE 1 in the files below.
  2. If currently impersonating another user, logging out of the site should only log out the impersonated user, not the true user. Exception: If the user logs out of the site from within the Admin console, he/she will be logged out entirely. In other words, if I'm logged in as myself (an administrator), and then I start impersonating a regular user (John Doe), when I log out of the site, I'll only be logging out as John Doe, so I'll be able to continue browsing the site as myself. If I then log out again, I'll be logging out as myself, so I'll be completely logged out. We accomplish this with a modification that would probably not be totally analogous for you guys with devise, but I’m sure it’s doable. See the methods marked FEATURE 2.

UX Notes

We also made some changes to the UI to make sure that administrators don't lose track of the fact that they're impersonating another user. When impersonating another user, we style the user actions menu (always visible in the upper right corner of the page) to be bright red. When the user hovers the mouse over this menu while impersonating a user, a red box pops up showing the name and user ID of the impersonated user. While this feature certainly isn't necessary, our admins find it very helpful.

class AdminController < ApplicationController
# FEATURE 1
# override current_user to ignore impersonation...
def current_user
@current_user ||= true_user
end
# FEATURE 2
# setting current_user should stop impersonation AND
# set the user, causing complete logout when calling current_user=(nil)
def current_user=(user)
if impersonating_user?
stop_impersonating_user!
end
self.true_user = user
@current_user = nil
end
end
class ApplicationController < ActionController::Base
# expose these methods as helper methods so that they can be used in views...
helper_method :current_user, :true_user, :impersonated_user, :impersonating_user?
# calling true_user should now do what current_user used to do...
alias_method :true_user, :current_user
# redefine current_user to first look for an impersonated user, and
# then to fall back on the old logic by calling true_user...
def current_user
@current_user ||= impersonated_user || true_user
end
# check the session for an impersonated user id. if there is one,
# find and return the associated User object, otherwise return nil...
def impersonated_user
@impersonated_user ||= begin
# if we have an impersonated user id in the session, but
# there is no logged-in user, clear the impersonated id...
if session[:impersonated_user_id] && !true_user
session[:impersonated_user_id] = nil
end
# if we have an impersonated user, get that user...
session[:impersonated_user_id] && User.active.find_by(id: session[:impersonated_user_id])
end
end
# returns TRUE if the logged-in user is currently
# impersonating another user, FALSE otherwise...
def impersonating_user?
impersonated_user.present?
end
# start impersonating the given user...
def impersonate_user!(user)
session[:impersonated_user_id] = user && user.id
@impersonated_user = user
@current_user = nil
end
# stop impersonation...
def stop_impersonating_user!
session[:impersonated_user_id] = nil
@impersonated_user = nil
@current_user = nil
end
# FEATURE 2
# setting the current user should first remove impersonation...
def current_user=(user)
if impersonating_user?
stop_impersonating_user!
else
self.true_user = user
@current_user = nil
end
end
end
class Admin::UsersController < AdminController
# when an administrator clicks the button/link to
# impersonate a user, this action is performed...
def impersonate
user = User.find(params[:id])
impersonate_user! user
redirect_to root_path, notice: "Impersonating #{user.first_name} #{user.last_name} (#{user.id})"
end
end
@brunowego
Copy link

@StephenRoos congrats! This is very interesting, I have been tried implement this, this gist will help me.

My API with Rails 5 uses devise_token_auth with ng-token-auth on frontend. My attempts for now have been frustrated.

I do not know how to propagate this to the frontend by updating the access token. More attempts I will make ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment