DEV Community

João Paulo Lethier for Zygo Tecnologia

Posted on • Originally published at Medium on

Autorização com cancancan no Rails — Verificando permissões

Autorização com cancancan no Rails — Verificando permissões

O código usado como exemplo para esse post está no github, no link https://github.com/jplethier/cancancan-examples

No post anterior sobre o cancancan, abordei a questão das definições das permissões na classe Ability, os métodos can e cannot, como combinar permissões e a precedência nesses casos, como utilizar um hash como terceiro parâmetro para definir mais condições, e o uso de alias para agrupar ações.

Nesse post, pretendo aprofundar um pouco em como verificar essas permissões nos controllers e nas views do projeto. Para entender melhor, vamos usar a mesma base de cenário do post anterior sobre definição de permissões, considerando a estrutura de Task e User. Abaixo temos a classe Ability definida no post anterior para facilitar o entendimento e lembrança nesse post:

https://medium.com/media/0561be6a3a53c5e9d491a58706785181/href

current_ability

Todo o funcionamento das verificações do cancancan dependem de um método chamado current_ability, que é um método de controller, também adicionado como helper, e que o cancancan adiciona no projeto automaticamente. O código desse método é o seguinte:

def current\_ability
  @current\_ability ||= Ability.new(current\_user)
end
Enter fullscreen mode Exit fullscreen mode

Como podemos perceber, esse método pressupões duas coisas:

  • Que a classe onde foram definidas as permissões se chama Ability;
  • E que existe um helper method no controller que se chame current_user, que retorne o usuário logado atualmente no sistema.

Por ter essas expectativas, em alguns casos é necessário sobrescrever esse método current_ability para ter o funcionamento correto. Por exemplo, caso tenha mais de um namespace no projeto e cada um tenha sua própria classe de permissões, como Web::Ability e API::Ability, ou tenham sido criadas classes específicas para cada context, como User::Ability e Task::Ability, será necessário sobrescrever o método para usar a classe certa em cada contexto(usando o API::Ability no API::ApplicationController, por exemplo).

No caso de não ter o método current_user, podemos tanto sobrescrever o método current_ability, como definir um alias para outro método. Por exemplo, seguindo o cenário de ter um API::ApplicationController, e imaginando que nesse contexto o método que retorna o usuário que está fazendo o request é chamado current_api_user, podemos escolher entre sobrescrever o método com Ability.new(current_api_user) ou colocar alias_method :current_user, :current_api_user no controller da API.

Verificando permissões nas views

Um dos usos mais comuns para verificação de permissões acontece nas views dos projetos. Por exemplo, quando precisamos montar um menu e definir quais itens do menu o usuário tem permissão de visualizar e/ou clicar. Ou em uma listagem onde cada item listado possui botões de ações de edição, remoção e visualização de detalhes, e precisamos saber se o usuário tem as permissões necessárias para executar essas ações.

Dado que o método current_ability existe e está definido corretamente, podemos utilizar outro helper do cancancan para nos ajudar nesses cenários: o método can?. Para exemplificar o uso desse método, vou usar o exemplo de uma listagem de tarefas:

https://medium.com/media/f402dac0daf271e3c1c12849684513d0/href

Como podemos ver no código acima, o uso do método can? é bem simples e intuitivo. Ele recebe a ação e o objeto a serem verificados a permissão, e esses dois parâmetros seguem a mesma ordem dos parâmetros do método can da classe Ability(visto no post anterior), com a ação sendo passada primeiro, e o objeto sendo o segundo parâmetro.

Por trás dos panos, quando utilizamos can? :approve, task, o que acontece é que o cancancan vai "perguntar" para o current_ability se o usuário logado pode executar a ação de aprovar essa determinada task. Ou seja, no final das contas, o método can? acaba sendo um próprio método da instância definida em Ability.new, ou seja, seria o mesmo que utilizar diretamente na view current_ability.can? :approve, task, mas a possibilidade de esconder o current_ability na chamada deixa as views muito mais limpas.

Verificando permissão sem ter um objeto específico

Um outro cenário que citei como muito comum ser necessário o uso das verificações de permissões é na construção de um menu de navegação. Vamos ver como ficaria um exemplo desse cenário:

https://medium.com/media/34fb6791f869420b0c52e119a9a7c8d7/href

Essa parte é um pouco mais confusa, pois ao invés de passarmos um objeto específico para o método can?, passamos uma classe para ele. O caminho para entendermos esse uso do can? é lermos ele como se fosse a seguinte pergunta: "O usuário logado atual tem permissão para ler alguma tarefa?".

O ponto chave é na palavra alguma , pois quando passamos para o ability uma classe ao invés de um objeto específico, ele vai nos retornar que a permissão existe desde que ela possivelmente exista para pelo menos um objeto daquela instância. Ok, o alguma está entendido, mas possivelmente? Como assim?

Para entender melhor, vamos a um exemplo. Imaginem um cenário onde um usuário comum(não é admin e nem moderador) não criou nenhuma tarefa ainda, ou seja, Task.where(user_id: user.id) retorna uma lista vazia. Nesse cenário, não existe nenhum registro no banco de dados de uma tarefa que o usuário tenha permissão para ler, porém, ao executar a verificação da permissão, o ability não irá no banco de dados. O que vai ser feito será simplesmente uma verificação de que existe uma permissão onde o usuário pode ler tarefas com uma condição específica, mas não será verificado no banco de dados se existe alguma tarefa nessa condição. Dessa forma o ability retorna que sim, o usuário pode ler alguma tarefa.

Uma forma de resumir melhor essa regra é pensar que a verificação feita é o oposto, ou seja, o que é verificado é se o usuário não pode ler nenhuma tarefa , e só é retornado falso para o can? caso o ability consiga confirmar com 100% de certeza essa afirmação de não ter permissão para nenhuma tarefa.

Para finalizar sobre a verificação de permissões nas views, é importante citar que também é possível utilizar o método cannot?. Como o próprio nome já deixa claro, ele é a negação do método can?, e na prática normalmente não é quase utilizado, sendo recomendado utilizar o unless can? ... sempre que possível.

Verificando permissões nos controllers

Quanto estamos escrevendo os controllers do projeto, temos duas preocupações em relação a autorização: a primeira é que precisamos verificar se o usuário pode chamar aquele endpoint, se pode executar aquela action do controller; a outra é que mesmo que ele possa executar a action, precisamos verificar quais objetos podem ser manipulados por esse usuário nessa action.

Seguindo o cenário usado no tópico anterior de listagem das tarefas, na action de index do TasksController, precisamos verificar se o usuário pode ler alguma tarefa, e em seguida precisamos saber quais tarefas ele pode ler. Isso pode ser feito com o código abaixo:

https://medium.com/media/662ad56be7137ae542375a1e5712d403/href

Aqui já temos o uso do outro helper adicionado ao controller pelo cancancan, assim como de um scope novo, também adicionado pela gem aos models do projeto.

O authorize! funciona de forma muito similar ao método can? usado nas views, onde ele recebe a ação e o objeto/classe a ser verificado junto com a ação. A diferença para o método can? é que o authorize! não retorna um booleano, mas sim joga a exceção Cancan::AccessDenied e aborta a execução do método com esse raise. Dessa forma, não precisamos colocar ifs em todos as actions, e podemos cuidar dessa exceção de forma centralizada, por exemplo, podemos colocar um rescue no ApplicationController e renderizar uma página genérica de acesso não autorizado sempre que um authorize! de alguma action jogar essa exceção.

Já o scope utilizado para buscar as tarefas no banco de dados, ele recebe o current_ability, e por default busca todas as instâncias do model onde está sendo chamado que podem ser lidos(a ação :index é utilizada como regra por default). Também é possível passar uma action específica como segundo parâmetro, por exemplo, poderíamos chamar Task.accessible_by(current_ability, :approve), e nesse caso ele retornaria todas as tasks que podem ser aprovadas de acordo com as permissões do current_ability. A query no banco de dados é montada usando as condições passadas como terceiro parâmetro(hash conditions) para o método can que define a permissão para a action requisitada(segundo parâmetro do scope, index por default caso o parâmetro não seja passado).

Usando before_actions para autorizar actions dos controllers

Usar o authorize! em todas as actions dos controllers pode se tornar muito repetitivo e verboso, além de ser muito propício a erros de desenvolvimento que gerariam bugs. Para facilitar essa verificação, o cancancan tem um outro helper, que pode ser adicionado no controller, onde o controller ficaria da seguinte forma:

https://medium.com/media/0e09570414e505a36e02e8364e13bcbd/href

Esses dois helpers utilizados(load_resource and authorize_resource) rodam como before_actions do controller, e fazem todas as verificações necessárias de permissões para carregar a lista(no caso do index) ou objeto(actions como show e edit, por exemplo) e também executam o authorize! para verificar a permissão de acesso na action do controller. Esses dois métodos podem ser utilizados de forma separada como mostrado acima, inclusive sendo utilizado somente um dos dois de forma isolada, como também podemos simplificar a chamada aos dois com o método load_and_authorize_resource no lugar de chamar os dois separadamente.

Assim como um before_action comum, esses três métodos aceitam um hash de condições, seja para definir que eles rodem em somente algumas actions, usando only: %i[index update] por exemplo, ou para definir que eles não rodam em algumas actions, except: %i[index update] . Existem outras opções também que podem ser passadas para sobrescrever o método find, o método new, e outras coisas. Para aprofundar em todas as regras e cenários, vale ler a wiki própria da gem sobre autorização nos controllers.

Fluxo de criação(new e create)

Em todos os exemplos anteriores, sempre falamos de como o cancancan facilita a busca no banco de dados e a verificação se existe permissão para o usuário atual executar a ação no(s) objeto(s) retornados do banco de dados. Mas e no caso das actions de new e create?

Nessas actions, o load_resource do cancancan instancia de forma automática o objeto correspondente ao model do controller, seguindo as seguintes regras:

  • Na action de new, o objeto é instanciado com as condições definidas no Ability
  • Na action de create, o objeto é instanciado com os parametros enviados do formulário

Vamos novamente para o controller de tarefas para olhar exemplos e tentar trazer mais clareza, considerando o usuário logado um usuário normal(nem moderador, nem admin):

https://medium.com/media/f3eb933ce7242c40fb94d54256325415/href

Nesse exemplo, considerando que definimos no ability que o user normal só pode criar tarefas para ele próprio, na action new a parte doload_resource funciona da mesma forma como se a gente executasse diretamente na action @task = Task.new(user_id: current_user.id), pois dado que a condição passada para o método can do ability foi user_id: user.id, ele criará a task com o id do user passado para o ability(no caso, o current_user). Dessa forma, ele garante que o objeto criado tem sempre as condições definidas na permissão e sem ter a necessidade de chamar o authorize! na instância recém criada.

Já no create, o cancancan espera que exista um método com o nome task_params(seguindo a convenção MODELNAME_params, nesse caso task sendo o nome do model) para instanciar o objeto usando Task.new(task_params). Em seguida, ele vai chamar o método authorize! para verificar a permissão de criação para o objeto instanciado com os parâmetros enviados, e se por algum motivo o usuário estiver tentando criar uma task com um user_id diferente do id dele, vai ser jogada a exceção Cancan::AccessDenied.

Fluxo de atualização ou remoção(show, edit, update e destroy)

As actions de show, edit, update e destroy funcionam de forma muito similar a action de create explicada acima. A diferença é somente na forma de popular a variável @task(no nosso caso do tasks controller). Ao invés de criar uma nova instância, é executado um find para buscar no banco. Esse find espera que as rotas sigam a convenção do rails com o id na url, e que o método find funcione conforme o esperado ao receber o id(ou seja, não tenha sido sobrescrito para funcionar de outra forma). Após o find, é executado o authorize! para verificar a permissão para a ação e o objeto encontrado.

É possível passar para o load_and_authorize_resource um parâmetro find_by: :method_name definindo um método customizado para ser usado para a busca no banco de dados.

Para mais clareza, segue abaixo o resultado final do tasks_controller com todas as actions de um CRUD criados e usando o load_and_authorize_resource:

https://medium.com/media/a9f3335821d649d5c8b390b267c1d711/href

Resumo

Tentei cobrir a maior parte de cenários e regras para verificação de permissões em um sistema, mas é uma parte muito diversa em cenários e contextos, então não tenho a presunção de ter conseguido cobrir todas as possibilidades e exceções. Para aprofundar mais, a documentação do próprio cancancan é um ótimo ponto de partida.

Sobre a lib em si, particularmente, na parte de verificações de permissões nos controllers é onde eu acho que o cancancan, com o uso do load_and_authorize_resource em um controller centralizado, mais é vantajoso e ajuda a manter o foco de desenvolvedor nas funcionalidades, evitando a tarefa repetitiva de verificar as permissões em todas as actions de todos os controllers. Isso facilita também manutenção de problemas relacionados as funcionalidades do sistema, e melhora a legibilidade, pois os controllers ficam menores e somente com o código relativo a sua função.

Isso ajuda a evitar erros de permissões também, pois ao centralizar e evitar a repetição de código, a parte de autorização do sistema é executada de forma uniforme em todos os controller, inclusive o controle da exceção jogada pelo cancancan, permitindo termos o sistema inteiro lidando com o acesso não autorizado de forma uniforme e centralizada também.

Apesar de todos os helpers e convenções da biblioteca, ela é bem flexível e fácil de ser customizada caso necessário, permitindo o uso para praticamente qualquer situação e contexto sem muita dor de cabeça também


Top comments (0)