ActiveSupport::CurrentAttributes e não se encaixa na autenticação que o Rails 8 gera.
Construir isso por conta própria acaba levando cerca de 30 linhas. Isso porque o código gerado é pequeno o suficiente para ler e estruturado para ser estendido.
O que este tutorial cobre
- Sobrescrever
Current.userpara que a impersonation percorra o código existente. - Manter
true_userdisponível para autorização e exibição. - A restrição de ordem em
resume_sessionque, caso contrário, bloquearia seu acesso.
Requisitos
Como o código gerado funciona
Dois arquivos importam.app/models/current.rb guarda o estado por requisição. Ele armazena a sessão e delega user a ela:
app/controllers/concerns/authentication.rb atribui essa sessão. require_authentication chama resume_session, que encontra o registro de sessão a partir de um cookie assinado. Quando não retorna nada, a requisição é enviada para a página de login.
O cookie é assinado, não criptografado, então um usuário pode ler o ID da sua sessão, mas não pode alterá-lo.
Sobrescrever Current.user
Tudo na aplicação já pergunta aCurrent.user quem está autenticado. Em vez de ensinar impersonation a todo esse código, intercepte-a em um único lugar.
Substitua a delegação por um método explícito:
Current.user agora retorna o usuário impersonado quando um está definido, e o usuário autenticado caso contrário. Sem nenhum dos dois, retorna nil, e a requisição é tratada como não autenticada, que é exatamente o que o restante do concern espera.
true_user não é opcional. Você precisa dele para o banner que informa a um administrador quem ele está impersonando. Você também precisa dele para a autorização.
Adicionar os métodos de impersonation
Emapp/controllers/concerns/authentication.rb:
Current é limpo após cada requisição, então o usuário impersonado precisa ser encontrado novamente a cada uma delas. A sessão do cookie carrega o ID entre as requisições. A guard clause em find_impersonated_user evita uma consulta por um usuário que não pode existir.
Note o que está deliberadamente ausente: nenhum registro de Session é criado. Impersonation não é um login. Se você criasse um, o cliente veria uma segunda sessão ativa na conta dele, o que seria preocupante e impreciso.
Integrar ao resume_session
stop_impersonating a terminate_session. Sem isso, sair e entrar novamente retoma a impersonation de quem você estava impersonando antes. O ID ainda está na sessão do cookie.
Adicionar o controller
Exibir no layout
Atribuindo atividade impersonada
A impersonation cria uma lacuna na sua telemetria. Todo trace registra o usuário impersonado, então a atividade de suporte fica indistinguível da atividade do próprio cliente. Feche essa lacuna marcando com tags as requisições com o ator real:Transcrição
Transcrito do vídeo e levemente editado. As legendas automáticas entenderam errado vários nomes de produtos e de APIs, e esses foram corrigidos.Ler a transcrição
Ler a transcrição
0:02 E aí, pessoal, neste episódio vamos adicionar impersonation ao gerador de autenticação do Rails 8. Na verdade, é bem mais simples do que você imaginaria. Então vamos direto ao ponto. Vou rodar
rails generate authentication para criar os modelos de user e session e todos os controllers e o módulo de autenticação que cuida da maior parte do trabalho. E vamos rodar rails db:migrate para criar essas tabelas no banco de dados.0:31 Depois vou rodar o console do Rails, e vamos criar dois usuários. Vamos fazer login como um, vamos impersonar o outro. Então User.create!, com o endereço de e-mail chris@gorails.com e a senha “password”. Depois fazemos a mesma coisa, mas com outro usuário com um endereço de e-mail diferente.0:51 Vou colocar qualquer senha nele, e isso já deve bastar. Então vamos dar uma olhada rápida na própria autenticação. Vamos em application.html.erb e escrevemos: se authenticated?, então podemos exibir current_user.email_address. Isso é o que veremos para o usuário atualmente logado, e queremos que isso mude quando estivermos impersonando um usuário diferente. Também vamos ter um botão para sair, que vai para o caminho da session com o método delete para nos deslogar.1:34 Ok, isso está bom. Devemos conseguir rodar nosso servidor Rails e abrir isso no navegador. Aí está meu teste anterior que eu tinha configurado. E aqui, sessions/new é para onde vamos ser levados. Quando vamos para a URL raiz, seremos redirecionados para cá.1:52 Eu já criei uma URL raiz principal para nós. Então ela nos leva para a autenticação, que é o que acontece por padrão. Toda página é autenticada, e você precisa dizer explicitamente que uma página não precisa de autenticação. Aqui podemos fazer login como chris@gorails.com, e pronto. chris@gorails.com, posso sair e entrar de novo.2:16 Vamos fazer isso mais uma vez para garantir que tudo funciona. Então, como fazemos autenticação com impersonation? Vamos dar uma olhada rápida na classe Current. É ela que cuida de armazenar a session e o usuário atuais para a requisição. Isso é limpo depois que a requisição termina, e simplesmente atribuímos a session e obtemos o usuário através dela.2:44 O módulo de autenticação é o responsável por atribuir essa session. Então, com require_authentication, ele vai chamar esse método, que vai retomar a session atual ou pedir que você se autentique. Retomar é tão simples quanto encontrar a session pelo cookie, o que vai buscar o registro de session no banco de dados com base no ID definido nos seus cookies. Eles são assinados para não poderem ser adulterados, mas continuam acessíveis para os usuários lerem, porque isso não importa, já que eles não conseguem fazer nada com isso para abusar. Então, para a impersonation, basicamente precisamos adicionar algo para interceptar isso.3:29 E quando pegamos o usuário atual, na verdade obtemos o usuário que estamos impersonando, não aquele com o qual estamos logados. E eu também não quero necessariamente criar uma session para ele, porque não estamos realmente logados como ele. Estamos só tentando fazer suporte ao cliente, por exemplo. Então vamos falar sobre como queremos que isso funcione. Em current.rb, esse delegate :user é exatamente o que precisamos interceptar.3:54 Se pensarmos dessa forma, poderíamos ter um atributo para o usuário impersonado, que atribuiríamos, e depois um método para user que retornaria o usuário impersonado ou o usuário da session. E se não houvesse session nem usuário impersonado, isso retornaria nil, e não estaríamos autenticados de forma alguma. Então isso funcionaria bem e faria com que também funcionasse com o módulo de autenticação. Então não precisamos mais realmente daquela linha que delega user, porque de qualquer forma vamos precisar basicamente sobrescrever isso aqui. Agora, poderíamos claro adicionar um método como true_user se quiséssemos, dizendo que esse é o usuário da session, e sempre conseguir pular o usuário impersonado, porque queremos que o usuário impersonado se comporte como nosso usuário atual, mas podemos precisar exibir um banner e mostrar o usuário original, para o qual você poderia adicionar um pequeno helper aqui também.5:01 Então isso é opcional, não necessariamente algo que precisamos, mas vamos entrar no nosso application.html.erb e adicionar algo que diga: se estiver impersonando, queremos ter um botão para parar de impersonar. Vamos precisar de algum tipo de caminho impersonate com o método delete também para parar a impersonation, e então podemos ter um botão para impersonar um usuário, que vai para o caminho impersonate, mas passaríamos um ID de um usuário. Isso provavelmente precisaria ser dinâmico na sua área de administração, e vamos fazer isso como uma requisição POST porque queremos criar a session de impersonation ali. Então precisamos implementar um punhado de métodos e um controller para conseguir fazer isso. Então vamos até nossas routes.5:58 Vamos adicionar um resource para impersonate. Podemos editar app/controllers/impersonates_controller.rb e criar esse ImpersonatesController, que vai herdar de ApplicationController. Uma coisa que queremos fazer é adicionar um require_admin aqui. Não temos uma flag de admin nos nossos usuários, mas isso é meio que um exemplo do que você poderia fazer.6:27 Então você poderia dizer require_admin, e isso verificaria se current_user é admin, e se você não for um admin, redireciona para o caminho ou URL raiz, o que você quiser fazer. Talvez você queira colocar um alerta ali ou algo assim, mas aqui precisamos criar nossas duas actions. Uma vai criar a session de impersonation, e a outra vai destruí-la. Isso vai ser algo como stop_impersonating, e vamos te redirecionar para a URL raiz, e para create vai ser bem simples, bem parecido: vamos dizer impersonate_user(User.find(params[:id])).7:13 Então você vai passar um ID na URL. Vamos então impersonar esse usuário e te redirecionar para a página inicial, então você vai ver a página inicial agora como esse usuário. Agora precisamos ir no módulo de autenticação e adicionar esses métodos. Então, se olharmos para esses outros métodos, podemos construir algo parecido. Precisamos do método impersonating?, que vai retornar verdadeiro ou falso.7:45 Precisamos de um impersonate_user, que vai configurar isso. Precisamos de um stop_impersonating. Não precisa de nada além disso. Mas também vamos precisar de um find_impersonated_user. Esse é o que vamos usar depois que você impersonar e for redirecionado para a próxima página.8:07 Essa requisição vai precisar encontrar o usuário impersonado, assim como encontra sua session atual com base no cookie. Então vamos começar simples. Vamos dizer: Current.impersonated_user está presente? Sim, você está impersonando.8:28 Não, você não está. Então, quando quisermos impersonar um usuário, vamos atribuir essa variável, impersonated_user igual a esse usuário. E também precisamos definir algo para que o ID de impersonated_user seja mantido para a próxima requisição. Vamos usar a session para isso. E na próxima requisição podemos verificar se há um ID na session para o ID do usuário impersonado.8:58 Se houver, podemos dizer find_by com o ID e tentar buscar esse usuário. Do jeito que estou configurando aqui, estamos verificando se esse ID está presente, e assim podemos evitar totalmente consultar o Active Record se não houver ID. Não precisamos consultar o banco de dados por um usuário cujo ID é null, porque não vamos obter nenhum usuário de volta para isso de qualquer forma. Então não faz sentido nem fazer essa consulta. Então vamos retornar o usuário ou nil, e então stop_impersonating é bem simples.9:29 O usuário impersonado agora deve ser nil, e queremos remover impersonated_user_id da session. Bem simples. Então esses são nossos principais itens de impersonation. Vou adicionar mais uma linha em branco aqui. Percebi que é assim que o módulo de autenticação já está organizado.9:51 Alguma organização com linhas em branco extras ali. E então precisamos integrar isso agora ao processo de autenticação de fato. E o principal processo de autenticação é esse método require_authentication: ou retoma uma session, ou reinicia uma nova. E envia o usuário para a página de login. Então resume_session é onde realmente precisamos modificar.10:10 Então vamos deixar Current.impersonated_user igual a find_impersonated_user. E depois vamos definir a session. É importante que essas duas linhas estejam nessa ordem específica, porque o valor de retorno de resume_session é o que determina se você é redirecionado para o login ou não. Se você colocasse isso depois, e não estivesse impersonando, continuaríamos te enviando para require_authentication, porque impersonated_user seria nil. Esse método retornaria nil aqui, o que é falsy, o que então dispara esse comportamento.10:48 E pareceria que você nunca consegue fazer login. Então é importante colocar essas coisas corretamente, para que o valor de retorno de resume_session esteja configurado direito. E se tudo correr bem, a única outra coisa de que realmente precisamos é um helper method para impersonating?, para que possamos usá-lo nas nossas views para exibir esse botão. Depois você provavelmente também vai querer verificar se o usuário é admin ou algo assim. E acabei de perceber que precisamos garantir que temos o ID do usuário como chave sempre que acessamos a session.11:27 Queremos garantir que isso seja consistente. Caso contrário, você vai ter problemas estranhos. Além disso, quando estamos encerrando a session, realmente queremos garantir que também paramos a impersonation, porque quando você sai e entra de novo, você não quer acabar logado na sua própria conta, mas automaticamente impersonando quem quer que você estivesse impersonando por último. Isso seria estranho. Então queremos garantir que paramos a impersonation também quando você sai.11:56 Então vamos testar isso. Vamos para o navegador. Estamos em sessions/new. Podemos fazer login, chris@gorails.com. Isso ainda nos loga, o que é bom.12:05 Não quebramos nada. Não estamos impersonando, então o botão de impersonar aparece. Podemos clicar nele. Isso vai fazer uma requisição POST para o controller de impersonation, que vai chegar em create. Vai ter o ID 2.12:20 Temos isso na URL para isso. Você também poderia configurar isso em users/:id/impersonate se quiser. Eu só coloquei tudo em um controller e você passa o ID. Dos dois jeitos funciona, mas conseguimos impersonar. E agora, Current.user.email_address é bob@bob.bob.12:37 Mas ainda estamos logados como chris@gorails.com. Então agora sabemos que estamos impersonando, porque vemos stop_impersonating. Se quisermos abrir o console do Rails, deixa eu aumentar a fonte. Podemos dizer Session.count. E o que veremos é que há apenas uma session para mim, chris@gorails.com.13:02 Se dissermos Session.first e pegarmos o usuário, vai ser o usuário número um, que não estou mostrando aqui. Está filtrado, mas o endereço de e-mail é chris@gorails.com. Então só eu estou logado, e minha impersonation não está sendo armazenada como uma session, o que seria estranho se um cliente visse que outra pessoa está logada na conta dele. Então não queremos realmente criar uma session para isso. Mas você poderia, se quisesse.13:31 E também marcar isso no registro. Você poderia adicionar algum campo extra ao modelo Session para manter um histórico dessas sessions de impersonation também. Tem muita coisa que você pode expandir a partir disso. Então agora, se pararmos de impersonar, isso vai definir Current.impersonated_user como nil, mas também remover impersonated_user_id da session. E voltaremos para chris@gorails.com.13:58 E posso trocar para bob@bob.bob a qualquer momento. E isso na verdade só exigiu modificar resume_session e terminate_session no final para incluir nossas mudanças de impersonation. E no nosso current.rb, em vez de fazer a delegação, vamos usar o usuário impersonado. E claro, sempre temos acesso a true_user também. Então, se formos no nosso application.html.erb, poderíamos dizer, se estiver impersonando, você provavelmente vai ter o endereço de e-mail do usuário atual ali.14:39 Mas talvez, se você estiver impersonando, você diria Current.true_user.email_address, se quiser mostrar o usuário real que você é. Agora veremos chris@gorails.com aparecer nesse lugar. Então você tem acesso aos dois tipos de usuário, assim como com a gem Pretender para Devise ou qualquer outra biblioteca de impersonation que existe para autenticação tradicional. Mas elas não funcionam com CurrentAttributes. E por isso é bem legal a gente conseguir construir isso sozinho com só um pouco de código.15:18 Não tem muita coisa aqui. E é fácil o suficiente de entender enquanto você olha e descobre como isso funciona. Então estou realmente satisfeito com como isso ficou. A autenticação do Rails 8 é muito, muito flexível. Você pode fazer todo tipo de coisa com ela e personalizá-la como quiser, seja para impersonation, OAuth, qualquer coisa.15:39 Tem todo tipo de flexibilidade ali. E é muito, muito fácil de ler com esse módulo de autenticação. Então é isso para este episódio. Espero que tenham gostado. E se vocês quiserem ver mais coisas assim, deixem saber nos comentários abaixo.15:56 E vamos usar isso como inspiração para nossas próximas lições. É isso. Um abraço. E a gente se fala depois. Até mais.