Pular para o conteúdo
_lvleo21
← Todos os artigos

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.

Referências