Como controlar o acesso por papéis com o Django Role Permissions?
Controle de acesso por papéis com django-role-permissions: roles, atribuição a usuários, verificação em views e templates, permissões por objeto e integração com Django REST Framework.
- django
- python
Publiquei este artigo originalmente no Medium em maio de 2020. Nesta versão revisei o código, corrigi dois erros no exemplo de template e acrescentei proteção de views, permissões por objeto e integração com o Django REST Framework. Os exemplos foram testados com Django 6.1, Python 3.13 e django-role-permissions 3.2.
Conforme um projeto Django cresce, aparece a necessidade de controlar o que cada usuário pode ver e acessar. Imagine alguém abrindo os seus dados pessoais no site do seu banco. O django-role-permissions resolve isso organizando as permissões por papel (role): você define os papéis do sistema, diz quais permissões cada um tem e depois pergunta, no código ou no template, se o usuário pode fazer determinada coisa.
Por baixo, a biblioteca usa o sistema de autenticação do próprio Django. Cada papel vira um Group e cada permissão vira uma Permission, então não há tabelas novas para migrar.
Instalação e configuração
pip install django-role-permissions
No settings.py, adicione o app:
INSTALLED_APPS = [
# ...
"rolepermissions",
]
Depois crie um arquivo roles.py na pasta do projeto, a mesma do settings.py, e aponte para ele no settings. Nos exemplos o projeto se chama escola:
ROLEPERMISSIONS_MODULE = "escola.roles"
Definindo os papéis
O exemplo é um sistema de notas escolares com dois tipos de usuário, professor e aluno. O aluno só pode ver a própria situação, enquanto o professor vê a de todos. Vamos chamar essas permissões de look_situation e look_all_situations.
No roles.py, cada papel é uma classe que herda de AbstractUserRole, e o atributo available_permissions lista as permissões daquele papel:
# escola/roles.py
from rolepermissions.roles import AbstractUserRole
class Professor(AbstractUserRole):
available_permissions = {
"look_all_situations": True,
}
class Aluno(AbstractUserRole):
available_permissions = {
"look_situation": True,
}
O True significa que o usuário recebe a permissão assim que ganha o papel. Uma permissão com False fica disponível para o papel, mas só passa a valer quando você a concede com grant_permission(user, "nome_da_permissao"). É útil para permissões que só alguns usuários daquele papel devem ter.
Se você alterar o roles.py com usuários já cadastrados, o comando python manage.py sync_roles cria os grupos que faltam no banco.
Atribuindo papéis aos usuários
Para dizer a qual papel um usuário pertence, use o assign_role, que recebe o usuário e o nome do papel:
from django.contrib.auth import get_user_model
from rolepermissions.roles import assign_role
User = get_user_model()
professor = User.objects.get(username="professor")
assign_role(professor, "professor")
aluno = User.objects.get(username="aluno")
assign_role(aluno, "aluno")
O nome do papel é o nome da classe em snake_case. Professor vira "professor", e uma classe CoordenadorPedagogico viraria "coordenador_pedagogico". Se preferir outro nome, defina o atributo role_name na classe. Para tirar um papel, existe o remove_role(user, "professor").
No original eu buscava os usuários com user.objects.get. O correto é obter o model com get_user_model(), que também funciona quando o projeto usa um model de usuário customizado.
Verificando permissões no backend
A verificação mais comum é o has_permission, que recebe o usuário e o nome da permissão:
from rolepermissions.checkers import has_permission, has_role
has_permission(aluno, "look_all_situations") # False
has_permission(professor, "look_all_situations") # True
has_role(aluno, "aluno") # True
has_role(aluno, ["aluno", "professor"]) # True se tiver qualquer um dos papéis
O primeiro retorno é False porque a pergunta é “o aluno tem a permissão look_all_situations?”, e essa permissão pertence só ao professor. O que fazer com a resposta depende da regra de negócio: um if, um filtro diferente no queryset, um erro.
Superusuários passam em todas as verificações por padrão. Se não quiser esse comportamento, defina ROLEPERMISSIONS_SUPERUSER_SUPERPOWERS = False no settings.
Protegendo views
Quase sempre a verificação serve para bloquear uma view inteira. Para isso a biblioteca traz mixins para class based views e decorators para function based views:
from django.views.generic import ListView
from rolepermissions.decorators import has_permission_decorator
from rolepermissions.mixins import HasPermissionsMixin
from .models import Situation
class AllSituationsView(HasPermissionsMixin, ListView):
required_permission = "look_all_situations"
model = Situation
@has_permission_decorator("look_all_situations")
def all_situations(request):
...
O mixin precisa vir antes da ListView na herança. Quando o usuário não tem a permissão, ou não está logado, a view responde 403. Para mandar o usuário à tela de login em vez disso, use redirect_to_login = True na classe ou ROLEPERMISSIONS_REDIRECT_TO_LOGIN = True no settings. Para checar papéis em vez de permissões, existem o HasRoleMixin, com o atributo allowed_roles, e o has_role_decorator.
Verificando no template
No HTML, carregue as template tags da biblioteca no começo do arquivo:
{% load permission_tags %}
O filtro has_role pergunta se o usuário tem algum dos papéis informados:
{% if request.user|has_role:"aluno,professor" %}
<h1>Aluno e professor podem ver este título</h1>
{% endif %}
O exemplo original tinha dois erros aqui. O espaço depois dos dois pontos (has_role: "...") faz o Django levantar TemplateSyntaxError. E o espaço depois da vírgula ("aluno, professor") faz o professor ficar de fora, porque o filtro separa os papéis pela vírgula sem remover espaços e procura um papel chamado " professor", que não existe. Escreva a lista sem espaços.
O filtro can pergunta pela permissão:
{% if request.user|can:"look_all_situations" %}
<h1>O professor pode ver todos os alunos e suas notas</h1>
{% endif %}
Permissões por objeto
A regra “o aluno não pode ver as notas dos colegas” não é resolvida só com look_situation. Todo aluno tem essa permissão, e o que muda é de qual situação estamos falando. Para regras que dependem do objeto, a biblioteca tem os object checkers.
Crie um permissions.py dentro do app. A biblioteca carrega esse arquivo automaticamente em todos os apps do INSTALLED_APPS:
# notas/permissions.py
from rolepermissions.permissions import register_object_checker
from escola.roles import Professor
@register_object_checker()
def view_situation(role, user, situation):
if role == Professor:
return True
return situation.student == user
O checker recebe o papel do usuário (a classe), o próprio usuário e o objeto. No backend ele é chamado pelo nome da função:
from rolepermissions.checkers import has_object_permission
has_object_permission("view_situation", aluno, situacao_do_aluno) # True
has_object_permission("view_situation", aluno, situacao_de_um_colega) # False
has_object_permission("view_situation", professor, situacao_de_um_colega) # True
No template, o can também funciona como tag para esse caso:
{% for situation in situations %}
{% can "view_situation" situation user=request.user as pode_ver %}
{% if pode_ver %}
<p>{{ situation.student }}: {{ situation.grade }}</p>
{% endif %}
{% endfor %}
Esconder itens no template não substitui filtrar o queryset. Em uma listagem, o aluno deve receber só as próprias situações (Situation.objects.filter(student=request.user)), e o checker fica para as telas de detalhe e para os pontos em que o objeto já está carregado.
Usando com Django REST Framework
A biblioteca não tem integração própria com o DRF, mas as funções de verificação encaixam direto em uma permission class:
# notas/api.py
from rest_framework.permissions import BasePermission
from rolepermissions import checkers
class SituationPermission(BasePermission):
def has_permission(self, request, view):
return checkers.has_permission(
request.user, "look_situation"
) or checkers.has_permission(request.user, "look_all_situations")
def has_object_permission(self, request, view, obj):
return checkers.has_object_permission("view_situation", request.user, obj)
Importar o módulo checkers, em vez das funções soltas, evita confundir o has_permission da biblioteca com o método de mesmo nome da classe. Com permission_classes = [SituationPermission] em um ModelViewSet, o aluno recebe 200 ao abrir a própria situação e 403 ao abrir a de um colega.
Manutenção e alternativas nativas
A última versão do django-role-permissions, a 3.2.0, saiu em junho de 2023. Ela continua funcionando com o Django 6.1, mas vale considerar isso antes de adotar a biblioteca em um projeto novo.
Como os papéis são grupos e as permissões são Permission do Django, as ferramentas nativas também enxergam o que a biblioteca cria. As permissões ficam associadas ao model de usuário, então professor.has_perm("auth.look_all_situations") retorna True, e {% if perms.auth.look_all_situations %} funciona no template. Se o projeto só precisa de poucos grupos fixos, dá para usar direto Group, PermissionRequiredMixin e @permission_required, sem dependência extra. A biblioteca compensa quando você quer declarar os papéis em código e ter os object checkers prontos.