Impersonando usuários com Devise

Tutoriais - 01/Ago/2019 - por Campus Code

A palavra "impersonar" não existe formalmente, é um anglicismo do termo impersonate que significa: personificar. Como é costume usar termos em inglês quando estamos codando, vamos usar impersonar neste artigo. ;)

Em aplicações de serviços mediados por software, impersonar se refere à habilidade de um usuário simular o comportamento de outro. É comum no desenvolvimento de aplicações ser necessário que um administrador do sistema realize ações em nome do usuário impersonado e também verifique a forma como ele está vendo as páginas da aplicação.

Existem muitas formas de implementar essa função na sua aplicação. Aqui vamos contar um pouco da nossa experiência utilizando a gem Pretender no nosso site www.treinadev.com.br, no qual utilizamos Ruby 2.6.2 e Rails 5.2.3.

Antes de começar, precisamos falar sobre como decidimos usar o Pretender. Assim como muitos outros sistemas, usamos o Devise para lidar com a autenticação de usuários. Por isso, começamos procurando soluções compatíveis com essa gem e descobrimos um artigo da própria Plataformatec com algumas sugestões de como fazer a personificação. Avaliamos as opções e escolhemos o Pretender entre as gems sugeridas. A alternativa seria implementar nosso próprio código, mas o Pretender se mostrou a forma mais rápida e prática para resolver o problema.

Decidida a estratégia, começamos com a instalação da gem. Adicionamos a seguinte linha no Gemfile (lembre de rodar o bundle install depois):

gem 'pretender'

E no seu ApplicationController:

class ApplicationController < ActionController::Base
  impersonates :user
end

Feita essa configuração, o método current_user agora retorna o usuário que está sendo impersonado. Além disso, você ganha novos métodos:

true_user
# retorna o verdadeiro usuário.

impersonate_user(user)
# começa a impersonar.

stop_impersonating_user
# para de impersonar.

Agora temos dois métodos que nos retornam users do Devise. O método true_user sempre retorna o usuário original e current_user mostra qual é o usuário da sessão atual. Assim, da comparação entre os métodos, podemos verificar se o administrador está impersonando algum usuário. Enquanto current_user for diferente de true_user, a personificação está ativa.

Utilizando impersonate_user e stop_impersonating_user podemos fazer as actions no UsersController. Na nossa aplicação, ele ficou mais ou menos assim:

class UsersController < ApplicationController
  before_action :authenticate_user!

...

  def impersonate
    user = User.find(params[:id])
    impersonate_user(user)
    redirect_to home_path
  end

  def stop_impersonating
    stop_impersonating_user
    redirect_to home_path
  end
end

Essas actions podem ser utilizadas em alguma view quando quisermos impersonar ou parar de impersonar um usuário. Na nossa aplicação, optamos por deixar um botão, visível apenas para admins, no perfil do usuário que será impersonado.

<% if current_user.admin? && current_user == true_user %>
  <%= link_to 'Impersonar', impersonate_user_path, method: :post %>
<% end %>

E na barra de navegação colocamos o botão para parar de impersonar quando um usuário está sendo impersonado:

<% if current_user != true_user %>
  <%= link_to 'Parar de impersonar' stop_impersonating_users_path, method: :delete %>
<% end %>

Finalmente, só resta definirmos as rotas! Nosso exemplo ficou assim:

resources :users, only: %i[show] do
  post :impersonate, on: :member
  delete :stop_impersonating, on: :collection
end

E está tudo pronto! Agora, quando um administrador impersona um usuário, ele é o true_user e o usuário impersonado será o current_user.

Tudo pronto mesmo?

Infelizmente, durante nossa implementação só isso não foi suficiente. Descobrimos que a forma como a gem trata o current_user entrava em conflito com o método que foi sobrescrito na nossa aplicação para atender a algumas necessidades. Por essa razão, perdemos algumas horas estudando o código da gem até finalmente conseguirmos adaptar nosso método com um User.find(request.session[:impersonated_user_id]) para que ele retornasse o usuário impersonado. Com sorte, esse não será seu caso. :)

Referências