Autorização com cancancan no Rails — Definindo permissões
Quando o assunto é autorização em projetos RubyOnRails, duas bibliotecas despontam como as mais utilizadas, conforme pode ser verificado no site ruby toolbox. No ruby toolbox é possível observar que apesar da gem pundit ter uma pontuação maior, a gem cancancan é a que tem mais downloads. A gem cancancan é um fork criado e continuado pela própria comunidade a partir da gem cancan criada anteriormente pelo Ryan Bates, mas que foi descontinuada. Nesse primeiro post sobre o cancancan vou falar um pouco mais de como funciona a definição de permissões utilizando essa biblioteca, usando o cenário descrito abaixo como exemplo.
O código usado como exemplo para esse post está no github, no link https://github.com/jplethier/cancancan-examples
Preparação
Para poder explicar melhor o uso e funcionamento da gem nesse post, vamos imaginar um projeto de gestão de tarefas onde existe os seguinte models task e user:
https://medium.com/media/9f3a9690682904cf7d9be85c460b7b7f/href
Instalação
Instalar é simples como a instalação de qualquer outra gem em um projeto RubyOnRails, basta adicionar gem 'cancancan' no Gemfile e rodar bundle install .
Definindo as permissões
A convenção do cancancan é que as permissões sejam definidas de forma centralizada em uma class Ability, que pode ser criada usando o comando rails g cancan:ability , onde ele cria um arquivo dentro da pasta app/models com o nome de ability.rb . Abaixo segue um exemplo da classe implementada nesse arquivo, considerando o contexto colocado anteriormente nesse post:
https://medium.com/media/0561be6a3a53c5e9d491a58706785181/href
Tem dois pontos importantes nessa classe. O primeiro é o include Cancan::Ability, que permite o uso dos métodos do cancancan utilizados dentro do initialize e também adiciona outros métodos que podem ser chamados diretamente nas instâncias dessa classe Ability, que falaremos mais abaixo.
Como podemos ver, apesar do nome da biblioteca ser cancancan , o include na classe Ability ainda usa o nome de módulo CanCan , preservando o nome do módulo originalmente da gem cancan que originou a versão atual da lib mantida pela comunidade.
O segundo ponto importante é dentro do initialize. Todas as permissões são definidas nesse método. E como funcionam essas definições? Basicamente, definimos todas as permissões usando os métodos can e cannot, onde eles recebem no mínimo 2 parâmetros, sendo o primeiro parâmetro a ação para a qual estamos definindo a permissão, e o segundo parâmetro o objeto onde a ação poderá ou não(no caso do cannot ) ser executada. Ainda é possível passar um hash de condições como terceiro parâmetro, definindo mais regras para a permissão que está sendo configurada. Ok, muito bonito, mas um pouco confuso, como funciona na prática?
Vamos usar a nossa classe acima como exemplo para entender melhor como funciona. Primeiro verificamos se o model user passado para o initialize não é nulo, caso seja nulo, que poderia ser no cenário de um visitante acessando um site na web, retornamos logo na primeira linha do método, sem definir nenhuma permissão, ou seja, no caso do visitante, não seria dada nenhuma permissão para ele.
Logo em seguida, usando o método can, damos uma permissão de manage em cima do objeto all caso o user seja um admin. Mas calma ai, ação manage e objeto all, o que seria isso? No caso do manage, o cancancan permite que você agrupe ações criando um alias para cada grupo, e nesse caso o manage corresponde a todas as ações que possam ser feitas em um objeto. Isso inclui todas as ações CRUD padrões e qualquer outra ação que venha a ser criada para o objeto passado. O all é um outro facilitador que o cancancan tem para nos permitir definir uma permissão para qualquer objeto. Ou seja, no nosso caso, se o usuário tiver o papel de admin, definimos que ele pode executar qualquer ação em qualquer objeto.
Passada a permissão dada para o admin, definimos uma permissão novamente de manage, dessa vez para o objeto Task com um hash de condições sendo passado dessa vez para definir regras mais específicas. No nosso caso, colocamos um hash com a chave user_id e o valor sendo o id do próprio usuário que estamos definindo as permissões no initialize(usando user.id). O hash de condições funciona de forma muito similar as condições passadas para montar queries do ActiveRecord, com exceção de que não podemos passar um objeto ActiveRecord como valor para esse hash, temos que passar o id(como foi feito exatamente nesse caso usando o atributo user_id e passando o user.id, ao invés de definir um hash como user: user). Com isso, essa permissão pode ser lida como definindo que o usuário pode gerenciar e executar qualquer ação em cima do objeto Task, desde que ele tenha o user_id igual ao id do próprio usuário, ou seja, de forma simples e menos técnica, definimos que o usuário pode gerenciar suas próprias tarefas.
Em seguida, criamos um alias para as ações de approve e reprove, e chamamos esse alias de moderate. Dessa forma, conseguimos definir as permissões para essas duas ações de forma conjunta passando a ação moderate para o método can. Caso tivéssemos optado por não usar o alias, o método can poderia receber uma lista de ações, ou seja, o primeiro parâmetro pode ser um array com as ações. Por exemplo, poderíamos usar can [:approve, :reprove] como primeiro parâmetro para o método.
Combinando permissões
Usando o alias criado, primeiro definimos que o moderador pode realizar as ações de aprovar e reprovar em qualquer tarefa. Na linha seguinte, usamos pela primeira vez o cannot, para definir que a não ser que o usuário seja um moderador, ele não pode moderar(aprovar e rejeitar) as tarefas. Aqui acontecem os primeiros "conflitos" de permissões:
- Temos uma permissão que define que o usuário pode realizar qualquer ação nas tarefas dele próprio, e depois usamos o método cannot para definir que o usuário não pode aprovar e nem reprovar as tarefas dele. https://medium.com/media/374fb3aec531ef89ef72c1a3808bf8f1/href
- O mesmo acontece com as permissões do moderador, onde primeiro definimos que o user que é moderador pode fazer as ações de aprovar e rejeitar em qualquer tarefa e, em seguida, definimos que ele não pode fazer essas ações nas tarefas dele.
O uso do método cannot é normalmente feito exatamente dessa forma, combinando permissões. Nesses casos, é muito importante a ordem em que as permissões são definidas, pois quando usamos can e cannot para definir regras para uma mesma ação e objeto, a última permissão definida vai sobrescrever a anterior, ou seja, no nosso caso o cannot prevalecee garante tanto que o usuário normal não consegue realizar aprovações e reprovações em nenhuma tarefa, assim como o moderador só consegue aprovar e reprovar tarefas de outros usuários, não conseguindo realizar essas ações nas tarefas próprias dele.
Por último, definimos uma regra simples usando o método can para garantir que o usuário tem a permissão necessária para gerenciar e editar suas informações pessoais, mas garantindo que não consegue editar dados de nenhum outro usuário, novamente utilizando o hash de condições como terceiro parâmetro para isso.
É preciso definir todas as ações existentes no sistema dentro dessa classe Ability?
Não, não é preciso. Quando uma ação não é definida explicitamente no Ability, o cancancan assume que o usuário não tem permissão para executar essa ação. Por exemplo, no cenário usado como contexto nesse post, não é definido de forma clara para ninguém a permissão de criação de usuários. Isso significa que ninguém tem essa permissão? No nosso caso não, pois definimos que o usuário que é admin tem permissão para executar qualquer ação em qualquer objeto(can :manage, :all), dessa forma, mesmo não estando explícito na classe Ability, o usuário com papel admin pode sim cadastrar um novo usuário. Em contrapartida, nem o moderador e nem o usuário comum podem realizar essa ação, pois mesmo que não tenha nada explicitamente definindo que eles não podem fazer, não tem nada também que dê permissão para eles realizarem, e nesse caso o cancancan entende que se não foi definida a permissão(nem dada, e nem proibida), o usuário não pode executar a ação.
Problemas para lidar com a classe Ability
Classe crescer demais com muitas permissões
É muito comum nossos projetos serem muito maiores e mais complexos do que o exemplo usado nesse post, e seguindo a convenção do cancancan de colocar todas as permissões dentro dessa mesma classe, ela tende a ficar muito grande e com regras muito complexas, dificultando o entendimento, manutenção e alteração no código. Para melhorar isso, recomendo fortemente quebrar em métodos, como por exemplo:
https://medium.com/media/f85069f557f2c82130de6715908962d2/href
Isso já ajuda em muitos cenários, mas nem sempre é suficiente, pode ser que a classe continue complexa e muito grande. Além disso, os testes unitários dessa classe tendem a ser muito extensos, pois testar todas as permissões para todos os possíveis papéis do usuário não é pouca coisa. O tamanho do arquivo de ability pode ter impacto inclusive na performance do seu projeto com a escala e aumento de models e ações do projeto. Nesses cenários, quebrar o arquivo de ability em vários arquivos é um caminho a ser considerado. Não irei aprofundar nesse tópico aqui, mas é possível ler mais sobre isso na própria wiki do cancancan aqui e no post CanCanCan that Scales do Alessandro Rodi.
Só consigo usar o model user dentro do ability, nenhum outro model pode ser passado para ele?
Por default e convenção, a classe ability é criada com o initialize recebendo somente o model user, mas é possível alterar isso sem grandes dores de cabeça. A única preocupação que precisamos ter ao alterarmos o initialize, colocando mais parâmetros nele, é sobrescrever o método helper que o cancancan nos dá para ser utilizado nos controllers e views, definindo ele com a utilização correta do ability criado no nosso projeto. Por exemplo, no cenário desse post, poderíamos ter um objeto Project e ter as permissões todas sendo definidas escopadas em relação a ele, e ser necessário passar o projeto que está sendo acessado no site no momento para o initialize, definindo algo como:
https://medium.com/media/f70848c1907e2af785a2b4a378014159/href
Resumo
Bom, pessoalmente eu gosto muito de utilizar o cancancan para definir as permissões dos meus projetos, acho ele um pouco complexo de entender no início, mas me sinto muito produtivo e acho que vai ficando mais fácil de entender e usar ele conforme o tempo. Sempre tento seguir as boas práticas definidas na wiki do projeto, e sempre quebro pelo menos em múltiplos métodos dentro do próprio ability para ajudar na legibilidade e manutenibilidade.
De fato as combinações de permissões podem ficar extensas e complexas, tornando necessário a quebra em múltiplas classes também, mas mesmo nesse caso o uso do cancancan não traz muitas dores de cabeça, pois mesmo fugindo da convenção padrão dele, é fácil sobrescrever os helpers dele e utilizar as classes customizadas criadas.
Próximos passos
Depois de definir todas as definições de permissões, o próximo passo é verificar essas permissões, principalmente nos controllers e nas views, mas pretendo escrever sobre isso em um próximo post somente.
Deixo aqui alguns links que usei como fonte para o post e como referência para maiores detalhes:
- CanCanCommunity/cancancan
- CanCanCommunity/cancancan
- CanCanCommunity/cancancan
- CanCanCommunity/cancancan
Top comments (0)