Gerenciamento de acesso no Expressjs com CASL



Em aplicativos modernos que oferecem suporte à autenticação, geralmente queremos alterar o que é visível para o usuário, dependendo de sua função. Por exemplo, um usuário convidado pode ver um artigo, mas apenas um usuário ou administrador registrado vê um botão para excluir este artigo.


Gerenciar essa visibilidade pode ser um pesadelo completo com funções crescentes. Você provavelmente escreveu ou viu um código como este:


if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

Esse código é distribuído por todo o aplicativo e geralmente se torna um grande problema quando o cliente altera os requisitos ou pede para adicionar funções adicionais. No final, você precisa passar por todos esses if-s e adicionar verificações adicionais.


Neste artigo, mostrarei uma maneira alternativa de implementar o gerenciamento de permissões na API do Expressjs usando uma biblioteca chamada CASL . Simplifica bastante o controle de acesso e permite reescrever o exemplo anterior para algo assim:


 if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

Novo no CASL? Eu recomendo a leitura O que é o CASL?


Nota: Este artigo foi publicado originalmente no Medium.


Aplicativo de demonstração


Como aplicativo de teste, criei uma API REST bastante simples para o blog . O aplicativo consiste em 3 entidades ( User , Post e Comment ) e 4 módulos (um módulo para cada entidade e outro para verificação de autorização). Todos os módulos podem ser encontrados na pasta src/modules .


O aplicativo usa modelos mangusto , autenticação passportjs e autorização baseada em CASL (ou controle de acesso). Usando a API, o usuário pode:


  • leia todos os artigos e comentários
  • criar um usuário (ou seja, registrar)
  • gerenciar seus próprios artigos (criar, editar, excluir), se autorizado
  • atualizar informações pessoais se autorizado
  • gerencie seus próprios comentários se autorizado

Para instalar este aplicativo, basta cloná-lo no github e execute o npm install e o npm start . Você também precisa iniciar o servidor MongoDB, o aplicativo se conecta ao mongodb://localhost:27017/blog . Depois que tudo estiver pronto, você poderá jogar um pouco e, para torná-lo mais divertido, importar os dados básicos da pasta db/ :


 mongorestore ./db 

Como alternativa, você pode seguir as instruções no arquivo de projeto README ou usar minha coleção Postman .


Qual é o truque?


Em primeiro lugar, a grande vantagem do CASL é que ele permite determinar os direitos de acesso em um único local, para todos os usuários! Em segundo lugar, o CASL não se concentra em quem é o usuário, mas no que ele pode fazer, ou seja, em suas capacidades. Isso permite distribuir esses recursos para diferentes funções ou grupos de usuários sem esforço desnecessário. Isso significa que podemos registrar direitos de acesso para usuários autorizados e não autorizados:


 const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() } 

Vamos agora analisar o código escrito acima. Uma instância do AbilityBuilder -a é criada na função defineAbilitiesFor(user) , seu método de extração divide esse objeto em 2 funções simples que can e cannot e uma matriz de rules ( cannot ser usada nesse código). Em seguida, usando as chamadas para a função can , determinamos o que o usuário pode fazer: o primeiro argumento passa a ação (ou uma matriz de ações), o segundo argumento é o tipo de objeto sobre o qual a ação é executada (ou uma matriz de tipos) e o terceiro argumento opcional pode ser passado ao objeto de condição. O objeto de condição é usado ao verificar os direitos de acesso em uma instância da classe, ou seja, verifica se a propriedade do author do objeto de post e user._id são iguais, se forem, true será retornado, caso contrário false . Para maior clareza, darei um exemplo:


 // Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) //  post.author === user._id,   true 

Em seguida, usando if (user) determinamos os direitos de acesso do usuário autorizado (se o usuário não estiver autorizado, não sabemos quem ele é e não temos um objeto com informações sobre o usuário). No final, retornamos uma instância da classe Ability , com a ajuda da qual iremos verificar os direitos de acesso.


Em seguida, criamos a constante ANONYMOUS_ABILITY , é uma instância da classe Ability para usuários não autorizados. No final, exportamos o middleware expresso, responsável pela criação de uma instância do Ability para um usuário específico.


API de teste


Vamos testar o que aconteceu com o Postman . Primeiro, você precisa acessar o AccessToken, para isso, envie uma solicitação:


 POST /session { "session": { "email": "casl@medium.com", "password": "password" } } 

Você recebe algo assim em resposta:


 { "accessToken": "...." } 

Esse token deve ser inserido no Authorization header da Authorization header e enviado com todas as solicitações subsequentes.


Agora vamos tentar atualizar o artigo.


 PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } } 

Tudo funciona bem. Mas e se atualizarmos o artigo de outra pessoa?


 PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" } 

Ocorreu um erro! Como esperado :)


Agora vamos imaginar que, para os autores do nosso blog, queremos criar uma página na qual eles possam ver todas as postagens que podem ser atualizadas. Do ponto de vista da lógica específica, isso não é difícil, basta selecionar todos os artigos nos quais o autor é igual a user._id . Mas já registramos essa lógica com a ajuda do CASL, seria muito conveniente obter todos esses artigos do banco de dados sem escrever solicitações extras e, se os direitos mudarem, a solicitação terá que ser alterada - trabalho extra :).


Felizmente, o CASL possui um pacote npm adicional - @ casl / mongoose . Este pacote permite consultar registros do MongoDB de acordo com permissões específicas! Para mangusto, este pacote fornece um plug-in que adiciona o método accessibleBy(ability, action) ao modelo. Usando esse método, também solicitaremos registros do banco de dados (leia mais sobre isso na documentação do CASL e no arquivo do pacote README ).


É exatamente assim que o handler de /posts implementado (também adicionei a capacidade de especificar para qual ação você precisa verificar as permissões):


 Post.accessibleBy(req.ability, req.query.action) 

Portanto, para resolver o problema descrito anteriormente, basta adicionar o parâmetro action=update :


 GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] } 

Em conclusão


Graças ao CASL, temos uma ótima maneira de gerenciar direitos de acesso. Tenho mais certeza que o tipo de construção


 if (ability.can('read', post)) ... 

muito mais claro e fácil do que


 if (user.role === ADMIN || user.auth && todo.author === user.id) ... 

Com o CASL, podemos ser mais claros sobre o que nosso código faz. Além disso, essas verificações certamente serão usadas em outras partes de nosso aplicativo e é aqui que o CASL ajudará a evitar a duplicação de código.


Espero que você esteja tão interessado em ler sobre o CASL quanto eu em criá-lo. O CASL possui uma documentação muito boa , provavelmente você encontrará muitas informações úteis, mas fique à vontade para fazer perguntas se estiver no bate - papo com gitter e adicionar um asterisco no github ;)

Source: https://habr.com/ru/post/pt414951/


All Articles