Sistemas Avançados de Permissões no Django 6.0: RLS, Políticas e Verificações Assíncronas para SaaS

Published on: 2025-12-25
Post image
pt django-60 sistema-de-permissoes saas-enterprise row-level-security rls politicas-de-acesso permissoes-assincronas multitenancy seguranca-backend arquitetura-django

Sistemas de permissões avançados em aplicações SaaS (Software como Serviço) corporativas precisam ir além do controle básico de login e senha. Em cenários com múltiplas organizações (multi-tenancy), papéis hierárquicos e regras de negócio variáveis, a autorização passa a ser uma camada central de segurança e governança.

Uma abordagem sólida combina três níveis complementares: isolamento de dados por organização, políticas que expressam regras de negócio e verificações assíncronas para validações complexas sem bloquear a aplicação. Essa combinação cria um modelo consistente, com boa performance e com baixa chance de vazamento de dados entre organizações.

Por que permissões tradicionais não são suficientes em SaaS corporativo

O sistema padrão de permissões do Django atende bem casos simples, como “usuário pode editar um modelo”. Em SaaS corporativo, o mesmo usuário costuma pertencer a uma organização, e os dados precisam ser isolados por esse limite. Além disso, papéis como administrador, gestor e membro exigem diferentes capacidades dentro do mesmo produto. Também surgem permissões por registro, como permitir editar apenas recursos criados por um usuário.

Outro fator é a presença de regras condicionais, como liberar ações apenas para organizações com determinado plano. Esse tipo de regra muda com contexto e é difícil de manter apenas com permissões fixas. Em sistemas grandes, ainda existe a necessidade de auditoria, para comprovar decisões de acesso. Por fim, checagens frequentes não podem degradar a performance em listas, APIs e operações em massa.

Visão geral da arquitetura: RLS, políticas e checagens assíncronas

Uma arquitetura em camadas reduz risco e melhora manutenção porque cada camada tem uma responsabilidade clara. A camada de segurança por linha (Row-Level Security, ou filtragem por linha) impede que consultas retornem dados de outra organização. A camada de políticas define se uma ação é permitida, traduzindo regras de negócio em lógica testável. A camada de checagens assíncronas trata validações custosas, como serviços externos, rate limiting e feature flags.

Essas camadas também se complementam contra falhas: mesmo que uma política esteja incorreta, a filtragem por organização reduz impacto. Da mesma forma, mesmo que uma query esteja mal construída, uma política pode impedir a execução da ação. Em ambientes corporativos, o objetivo é “falhar fechado”, ou seja, negar quando há dúvida. Essa separação também facilita testes e observabilidade.

Fundação de multi-tenancy com modelos base

Multi-tenancy significa que uma única instância do sistema atende múltiplas organizações, mantendo dados separados. Um modelo Organization representa o “inquilino” (tenant) e fica associado a usuários e recursos. Um campo role no usuário define o papel dentro da organização, como admin, manager e member. Um modelo base abstrato, como TenantAwareModel, padroniza campos comuns, como organization e created_by.

A padronização reduz inconsistências ao longo do domínio e ajuda a manter regras uniformes. Índices no banco tornam consultas por organização mais rápidas e previsíveis. Também é comum separar um manager “com filtro” do manager “sem filtro” para cenários administrativos. A seguir, o código demonstra a estrutura mínima para suportar o restante do sistema.

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.indexes import GinIndex


class Organization(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=["slug"]),
        ]

    def __str__(self) -> str:
        return self.name


class User(AbstractUser):
    organization = models.ForeignKey(
        Organization,
        on_delete=models.CASCADE,
        related_name="members",
    )
    role = models.CharField(
        max_length=50,
        choices=[
            ("admin", "Administrador"),
            ("manager", "Gestor"),
            ("member", "Membro"),
        ],
        default="member",
    )


class TenantAwareModel(models.Model):
    organization = models.ForeignKey(
        Organization,
        on_delete=models.CASCADE,
    )
    created_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True
        indexes = [
            models.Index(fields=["organization", "created_at"]),
        ]


class Project(TenantAwareModel):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)

    class Meta:
        db_table = "projects"
        indexes = [
            models.Index(fields=["organization", "created_at"]),
            models.Index(fields=["organization", "created_by"]),
            GinIndex(fields=["organization"]),
        ]

    def __str__(self) -> str:
        return self.name

RLS na prática: filtragem automática por organização com Manager

Row-Level Security, no sentido amplo, é garantir que consultas retornem apenas linhas permitidas para o contexto atual. Em PostgreSQL, existe RLS nativa, mas também é possível implementar uma forma efetiva no Django por meio de managers customizados. Um manager é o componente que controla como um queryset é criado, permitindo inserir filtros padrão. Ao acoplar o contexto de organização ao manager, todas as consultas passam a respeitar automaticamente o tenant.

Essa técnica reduz a chance de esquecimentos em filtros manuais. Ainda assim, é importante manter um caminho de bypass para operações administrativas internas. A implementação abaixo usa um atributo de organização no manager e o aplica no get_queryset. Em seguida, é criado um manager alternativo sem filtro para uso controlado.

from django.db import models


class TenantManager(models.Manager):
    def __init__(self, *args, **kwargs):
        self._organization = None
        super().__init__(*args, **kwargs)

    def set_organization(self, organization):
        self._organization = organization
        return self

    def get_queryset(self):
        qs = super().get_queryset()
        if self._organization is not None:
            return qs.filter(organization=self._organization)
        return qs
from django.db import models


class Project(TenantAwareModel):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)

    objects = TenantManager()
    all_objects = models.Manager()  # bypass controlado

    class Meta:
        db_table = "projects"

Contexto de organização via middleware e cuidados com concorrência

Para aplicar o filtro automaticamente, um middleware pode capturar a organização do usuário autenticado e configurar o manager. Middleware é um componente executado em cada requisição, antes e depois da view, permitindo preparar contexto. Uma forma tradicional usa thread-local, que armazena valores por thread. Em ambientes com ASGI e concorrência assíncrona, thread-local pode ser insuficiente, e a alternativa moderna é usar contextvars.

Mesmo em modelos síncronos, o middleware precisa limpar o contexto ao final para evitar vazamento entre requisições. Também é importante que o filtro seja aplicado apenas quando existe usuário autenticado. A seguir, um exemplo com thread-local para simplificar o conceito, mantendo a limpeza no process_response. Em cenários assíncronos avançados, a troca para contextvars preserva o mesmo desenho com segurança maior.

from threading import local
from django.utils.deprecation import MiddlewareMixin
from django.apps import apps

from .managers import TenantManager

_thread_locals = local()


def get_current_organization():
    return getattr(_thread_locals, "organization", None)


def set_current_organization(organization):
    _thread_locals.organization = organization


class TenantMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.user.is_authenticated:
            org = request.user.organization
            set_current_organization(org)

            for model in apps.get_models():
                if hasattr(model, "objects") and isinstance(model.objects, TenantManager):
                    model.objects.set_organization(org)

    def process_response(self, request, response):
        set_current_organization(None)
        return response

Políticas de autorização: regras de negócio em componentes testáveis

Políticas descrevem “quem pode fazer o quê” em termos de regras de negócio, separadas de controllers e de consultas. Um padrão útil é criar um contexto de permissão, contendo usuário, recurso e ação, e avaliá-lo com várias políticas. Uma política pode cobrir hierarquia de papéis, outra pode cobrir posse do recurso, e outra pode assegurar pertencimento à organização. Esse desenho reduz ifs espalhados e melhora a consistência do sistema.

Uma enumeração de ações evita strings soltas e facilita mapear ações de views e APIs. Um dataclass de contexto torna o transporte de dados explícito e simples. O uso de Protocol define um “contrato” para políticas, incentivando padronização. A seguir, a base do motor de políticas e três políticas comuns em SaaS corporativo.

from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol


class Action(Enum):
    VIEW = "view"
    CREATE = "create"
    UPDATE = "update"
    DELETE = "delete"
    MANAGE = "manage"


@dataclass
class PermissionContext:
    user: Any
    resource: Any = None
    action: Action = None
    extra: dict | None = None


class Policy(Protocol):
    def check(self, context: PermissionContext) -> bool: ...
    def get_reason(self) -> str: ...
class RoleBasedPolicy:
    ROLE_HIERARCHY = {
        "admin": ["manage", "delete", "update", "create", "view"],
        "manager": ["update", "create", "view"],
        "member": ["view"],
    }

    def __init__(self):
        self._reason = ""

    def check(self, context: PermissionContext) -> bool:
        user_role = getattr(context.user, "role", None)
        required_action = context.action.value if context.action else None

        allowed_actions = self.ROLE_HIERARCHY.get(user_role, [])
        if required_action in allowed_actions:
            return True

        self._reason = f"Papel '{user_role}' não pode executar '{required_action}'"
        return False

    def get_reason(self) -> str:
        return self._reason


class OwnershipPolicy:
    def __init__(self):
        self._reason = ""

    def check(self, context: PermissionContext) -> bool:
        if context.action == Action.VIEW:
            return True

        if context.resource is None:
            return True

        if hasattr(context.resource, "created_by") and context.resource.created_by == context.user:
            return True

        self._reason = "A alteração é permitida apenas no recurso criado pelo próprio usuário"
        return False

    def get_reason(self) -> str:
        return self._reason


class OrganizationPolicy:
    def __init__(self):
        self._reason = ""

    def check(self, context: PermissionContext) -> bool:
        if context.resource is None:
            return True

        if hasattr(context.resource, "organization"):
            if context.resource.organization == context.user.organization:
                return True

            self._reason = "O recurso pertence a outra organização"
            return False

        return True

    def get_reason(self) -> str:
        return self._reason

Avaliador de políticas e cache para performance

Um avaliador central executa várias políticas e decide se o acesso é permitido. O padrão mais seguro é “todas as políticas precisam passar” para conceder permissão. Para performance, resultados podem ser armazenados em cache, já que permissões são verificadas com alta frequência. A chave de cache precisa refletir usuário, papel, ação e recurso, evitando reutilizar decisões incorretas.

Um cache em memória (LRU) é útil localmente, mas em produção costuma ser substituído por Redis para compartilhar entre processos. Um cuidado essencial é invalidar cache quando papéis, vínculos de organização ou propriedade do recurso mudam. Outra medida é escolher um TTL (tempo de vida) curto o suficiente para reduzir inconsistência. O exemplo abaixo organiza o avaliador e deixa o encaixe de cache explícito.

import hashlib
import json
from typing import List, Tuple

from .policies import Policy, PermissionContext


class PermissionEvaluator:
    def __init__(self, policies: List[Policy], cache_backend=None):
        self.policies = policies
        self.cache = cache_backend  # pode ser Redis ou None

    def evaluate(self, context: PermissionContext) -> Tuple[bool, str]:
        cache_key = self._make_cache_key(context)

        if self.cache is not None:
            cached = self.cache.get(cache_key)
            if cached is not None:
                return tuple(cached)

        for policy in self.policies:
            if not policy.check(context):
                result = (False, policy.get_reason())
                if self.cache is not None:
                    self.cache.set(cache_key, result)
                return result

        result = (True, "")
        if self.cache is not None:
            self.cache.set(cache_key, result)
        return result

    def _make_cache_key(self, context: PermissionContext) -> str:
        data = {
            "user_id": getattr(context.user, "id", None),
            "user_role": getattr(context.user, "role", None),
            "org_id": getattr(getattr(context.user, "organization", None), "id", None),
            "resource_id": getattr(context.resource, "id", None),
            "resource_type": type(context.resource).__name__ if context.resource else None,
            "action": context.action.value if context.action else None,
        }
        serialized = json.dumps(data, sort_keys=True)
        return hashlib.md5(serialized.encode("utf-8")).hexdigest()
from .policies import OrganizationPolicy, RoleBasedPolicy, OwnershipPolicy

STANDARD_EVALUATOR = PermissionEvaluator([
    OrganizationPolicy(),
    RoleBasedPolicy(),
])

RESOURCE_EVALUATOR = PermissionEvaluator([
    OrganizationPolicy(),
    RoleBasedPolicy(),
    OwnershipPolicy(),
])

Verificações assíncronas: validações externas, rate limiting e feature flags

Verificações assíncronas servem para tarefas que podem esperar sem bloquear a aplicação, como chamadas a serviços externos. Em Django com ASGI, views assíncronas conseguem aguardar essas operações e liberar o loop de eventos para outras requisições. Rate limiting é a limitação de frequência de ações para reduzir abuso e ataques. Feature flag é uma “chave” de ativação de recurso por organização, comum em produtos por planos.

Um padrão eficiente é executar checagens em paralelo com asyncio.gather e consolidar o resultado. Erros de rede ou exceções devem negar acesso por padrão, mantendo o princípio de falhar fechado. Também é importante diferenciar checagens síncronas simples das assíncronas, evitando overhead desnecessário. O código abaixo organiza um verificador assíncrono com checagens exemplificadas.

import asyncio
from typing import Any, Optional, Tuple

from .policies import Action


class AsyncPermissionChecker:
    @staticmethod
    async def check_external_service(user_id: int, resource_id: int) -> bool:
        await asyncio.sleep(0.1)  # simula chamada externa
        return True

    @staticmethod
    async def check_rate_limit(user_id: int, action: str) -> bool:
        await asyncio.sleep(0.01)  # simula consulta a Redis
        return True

    @staticmethod
    async def check_feature_flag(org_id: int, feature: str) -> bool:
        await asyncio.sleep(0.01)  # simula serviço de flags
        return True

    @classmethod
    async def comprehensive_check(
        cls,
        user: Any,
        resource: Any,
        action: Action,
    ) -> Tuple[bool, Optional[str]]:
        resource_id = getattr(resource, "id", None)

        results = await asyncio.gather(
            cls.check_external_service(user.id, resource_id or 0),
            cls.check_rate_limit(user.id, action.value),
            cls.check_feature_flag(user.organization.id, f"action_{action.value}"),
            return_exceptions=True,
        )

        for i, result in enumerate(results):
            if isinstance(result, Exception):
                return False, f"Falha na checagem assíncrona {i}: {str(result)}"
            if result is False:
                return False, f"Checagem assíncrona {i} negou a ação"

        return True, None

Decorators para views: integração limpa com checagens síncronas e assíncronas

Decorators são funções que “envolvem” views para executar lógica antes da execução principal. Com eles, a autorização fica padronizada e reduz duplicação de código. Uma variante simples verifica apenas ação e papel, sem recurso específico. Outra variante carrega um recurso e aplica políticas de nível de registro, como posse e organização. Para views assíncronas, um decorator pode executar a avaliação síncrona e, em seguida, disparar checagens assíncronas.

Em termos de segurança, a busca do recurso precisa respeitar o filtro por tenant, evitando recuperar objetos de outra organização. Quando RLS por manager está ativo, model_class.objects já filtra por organização. Também é importante que a negativa de permissão retorne um erro consistente, como PermissionDenied. A seguir, decorators prontos para uso em views Django.

from functools import wraps

from django.core.exceptions import PermissionDenied

from .evaluator import STANDARD_EVALUATOR, RESOURCE_EVALUATOR
from .policies import Action, PermissionContext
from .async_checks import AsyncPermissionChecker


def require_permission(action: Action, evaluator=STANDARD_EVALUATOR):
    def decorator(view_func):
        @wraps(view_func)
        def wrapped_view(request, *args, **kwargs):
            context = PermissionContext(user=request.user, action=action)
            allowed, reason = evaluator.evaluate(context)
            if not allowed:
                raise PermissionDenied(reason)
            return view_func(request, *args, **kwargs)

        return wrapped_view

    return decorator


def require_resource_permission(
    action: Action,
    resource_param: str = "pk",
    model_class=None,
    evaluator=RESOURCE_EVALUATOR,
):
    def decorator(view_func):
        @wraps(view_func)
        def wrapped_view(request, *args, **kwargs):
            resource_id = kwargs.get(resource_param)
            resource = model_class.objects.get(pk=resource_id)

            context = PermissionContext(
                user=request.user,
                resource=resource,
                action=action,
            )
            allowed, reason = evaluator.evaluate(context)
            if not allowed:
                raise PermissionDenied(reason)

            return view_func(request, *args, **kwargs)

        return wrapped_view

    return decorator


def require_async_permission(action: Action):
    def decorator(view_func):
        @wraps(view_func)
        async def wrapped_view(request, *args, **kwargs):
            context = PermissionContext(user=request.user, action=action)
            allowed, reason = STANDARD_EVALUATOR.evaluate(context)
            if not allowed:
                raise PermissionDenied(reason)

            resource = kwargs.get("resource")
            allowed, reason = await AsyncPermissionChecker.comprehensive_check(
                request.user,
                resource,
                action,
            )
            if not allowed:
                raise PermissionDenied(reason or "Acesso negado")

            return await view_func(request, *args, **kwargs)

        return wrapped_view

    return decorator

Uso nas views: listagem, detalhe, atualização e criação assíncrona

Em listagens, o maior ganho vem do filtro automático por organização, evitando filtros repetidos e reduzindo risco. Em detalhes e atualizações, a permissão precisa considerar o recurso específico, pois a posse ou estado do recurso pode influenciar. A criação assíncrona é útil quando o fluxo depende de validações externas antes de efetivar a operação. O conjunto a seguir mostra usos típicos em views baseadas em função.

Quando o manager com RLS está ativo, Project.objects.all() retorna apenas itens da organização corrente. Em project_detail e project_update, o decorator busca o recurso com model_class.objects.get, já filtrado por tenant. Em fluxos de POST, a autorização precisa acontecer antes da alteração. Em views assíncronas, a autorização combina políticas locais e checagens externas paralelizáveis.

from django.shortcuts import get_object_or_404, render, redirect
from django.views.decorators.http import require_http_methods

from .models import Project
from .permissions.decorators import (
    require_permission,
    require_resource_permission,
    require_async_permission,
)
from .permissions.policies import Action


@require_permission(Action.VIEW)
def project_list(request):
    projects = Project.objects.all()
    return render(request, "projects/list.html", {"projects": projects})


@require_resource_permission(
    Action.VIEW,
    resource_param="project_id",
    model_class=Project,
)
def project_detail(request, project_id):
    project = get_object_or_404(Project, pk=project_id)
    return render(request, "projects/detail.html", {"project": project})


@require_resource_permission(
    Action.UPDATE,
    resource_param="project_id",
    model_class=Project,
)
@require_http_methods(["POST"])
def project_update(request, project_id):
    project = get_object_or_404(Project, pk=project_id)
    project.name = project.name  # atualização real entraria aqui
    project.save(update_fields=["name"])
    return redirect("project_detail", project_id=project.id)


@require_async_permission(Action.CREATE)
async def project_create_async(request):
    if request.method == "POST":
        # lógica assíncrona de criação entraria aqui
        pass
    return render(request, "projects/create.html")

Integração com Django REST Framework (DRF) via classe de permissão

Em APIs, o Django REST Framework usa classes de permissão para autorizar requisições. Uma classe customizada pode mapear ações do ViewSet (list, retrieve, create, update, destroy) para ações do motor de políticas. A checagem pode ser dividida entre permissão geral (has_permission) e permissão por objeto (has_object_permission). Isso mantém a autorização consistente entre páginas HTML e endpoints REST.

Em listagens, normalmente basta validar se o usuário pode “ver” o tipo de recurso, pois o filtro por tenant limita o queryset. Em operações por objeto, a política de posse e organização se torna relevante. Também é importante que perform_create injete organization e created_by, evitando que o cliente envie dados de tenant. O exemplo a seguir mostra uma permissão dinâmica e um ViewSet com criação segura.

from rest_framework import permissions, viewsets

from .evaluator import RESOURCE_EVALUATOR
from .policies import Action, PermissionContext
from ..models import Project
from ..serializers import ProjectSerializer


class DynamicPermission(permissions.BasePermission):
    action_map = {
        "list": Action.VIEW,
        "retrieve": Action.VIEW,
        "create": Action.CREATE,
        "update": Action.UPDATE,
        "partial_update": Action.UPDATE,
        "destroy": Action.DELETE,
    }

    def has_permission(self, request, view):
        action = self.action_map.get(getattr(view, "action", None), Action.VIEW)
        context = PermissionContext(user=request.user, action=action)
        allowed, _ = RESOURCE_EVALUATOR.evaluate(context)
        return allowed

    def has_object_permission(self, request, view, obj):
        action = self.action_map.get(getattr(view, "action", None), Action.VIEW)
        context = PermissionContext(user=request.user, resource=obj, action=action)
        allowed, _ = RESOURCE_EVALUATOR.evaluate(context)
        return allowed


class ProjectViewSet(viewsets.ModelViewSet):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    permission_classes = [DynamicPermission]

    def perform_create(self, serializer):
        serializer.save(
            organization=self.request.user.organization,
            created_by=self.request.user,
        )

Cache com Redis para decisões de permissão e invalidação

Redis é um armazenamento em memória muito usado como cache distribuído em sistemas escaláveis. O cache de permissões reduz recomputação de políticas em pontos quentes, como listagens e APIs com alto volume. Um TTL de alguns minutos costuma equilibrar performance e consistência. A invalidação por usuário é útil quando papéis mudam, embora padrões de invalidação mais sofisticados possam ser necessários em sistemas grandes.

A chave deve ser estável e calculada pelo avaliador, enquanto o cache apenas armazena e devolve a decisão. A serialização em JSON facilita interoperabilidade e inspeção. Também é importante separar um banco (db) no Redis para evitar colisões com outros caches do sistema. O exemplo abaixo fornece um backend simples de cache para ser injetado no PermissionEvaluator.

import json
import redis
from django.conf import settings


redis_client = redis.Redis(
    host=getattr(settings, "REDIS_HOST", "localhost"),
    port=getattr(settings, "REDIS_PORT", 6379),
    db=getattr(settings, "REDIS_PERMISSION_DB", 3),
    decode_responses=True,
)


class PermissionCache:
    TTL = 300

    @staticmethod
    def get(key: str):
        value = redis_client.get(f"perm:{key}")
        if value:
            return json.loads(value)
        return None

    @staticmethod
    def set(key: str, value: tuple):
        redis_client.setex(
            f"perm:{key}",
            PermissionCache.TTL,
            json.dumps(value),
        )

    @staticmethod
    def invalidate_user(user_id: int):
        pattern = f"perm:*{user_id}*"
        for key in redis_client.scan_iter(match=pattern):
            redis_client.delete(key)

Testes automatizados: cobrindo papéis, posse e avaliação combinada

Testes de permissão garantem que mudanças futuras não criem regressões silenciosas de segurança. O foco deve ser validar regras de papel, regras de posse e a composição das políticas no avaliador. É importante incluir cenários permitidos e negados, pois ambos fazem parte do contrato de segurança. Em bancos com multi-tenancy, também é comum testar se recursos de outra organização são bloqueados.

Pytest é frequentemente usado com Django por ser conciso e expressivo. A criação de fixtures com organização, usuários e recursos torna o teste legível. O objetivo é manter testes rápidos e determinísticos, evitando chamadas externas. O exemplo a seguir cobre política por papel, política de posse e a avaliação combinada.

import pytest
from django.test import TestCase

from .models import Organization, User, Project
from .permissions.policies import Action, PermissionContext, RoleBasedPolicy, OwnershipPolicy
from .permissions.evaluator import PermissionEvaluator


@pytest.mark.django_db
class TestPermissionSystem(TestCase):
    def setUp(self):
        self.org = Organization.objects.create(name="Org Teste", slug="org-teste")

        self.admin = User.objects.create(
            username="admin",
            organization=self.org,
            role="admin",
        )
        self.member = User.objects.create(
            username="membro",
            organization=self.org,
            role="member",
        )

        self.project = Project.objects.create(
            name="Projeto Teste",
            organization=self.org,
            created_by=self.member,
        )

    def test_role_based_policy(self):
        policy = RoleBasedPolicy()

        context_admin = PermissionContext(user=self.admin, action=Action.DELETE)
        assert policy.check(context_admin) is True

        context_member = PermissionContext(user=self.member, action=Action.DELETE)
        assert policy.check(context_member) is False

    def test_ownership_policy(self):
        policy = OwnershipPolicy()

        context_owner = PermissionContext(
            user=self.member,
            resource=self.project,
            action=Action.UPDATE,
        )
        assert policy.check(context_owner) is True

        context_not_owner = PermissionContext(
            user=self.admin,
            resource=self.project,
            action=Action.UPDATE,
        )
        assert policy.check(context_not_owner) is False

    def test_combined_evaluation(self):
        evaluator = PermissionEvaluator([RoleBasedPolicy(), OwnershipPolicy()])

        context = PermissionContext(
            user=self.member,
            resource=self.project,
            action=Action.UPDATE,
        )
        allowed, _ = evaluator.evaluate(context)
        assert allowed is True

Auditoria e rastreabilidade: registrando decisões de acesso

Auditoria é o registro de decisões de permissão para rastrear incidentes e cumprir requisitos de conformidade. Em sistemas corporativos, registrar apenas falhas nem sempre é suficiente, pois decisões permitidas também precisam de rastreabilidade. Um modelo de log costuma guardar usuário, ação, tipo e id do recurso, decisão e motivo. O IP e o timestamp ajudam a correlacionar eventos de segurança.

Além do registro no banco, logs de aplicação permitem envio para ferramentas de monitoramento. O acoplamento pode ser mantido baixo com uma função que recebe contexto e resultado. A indexação por usuário e tempo facilita consultas e investigações. O exemplo abaixo modela uma tabela de auditoria e uma função de escrita.

import logging
from django.db import models

from .models import User
from .permissions.policies import PermissionContext

logger = logging.getLogger("permissions")


class PermissionAuditLog(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    action = models.CharField(max_length=50)
    resource_type = models.CharField(max_length=100, blank=True)
    resource_id = models.IntegerField(null=True, blank=True)
    allowed = models.BooleanField()
    reason = models.TextField(blank=True)
    ip_address = models.GenericIPAddressField()
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=["user", "timestamp"]),
            models.Index(fields=["timestamp"]),
        ]


def audit_permission_check(
    context: PermissionContext,
    allowed: bool,
    reason: str,
    ip_address: str,
):
    PermissionAuditLog.objects.create(
        user=context.user,
        action=context.action.value if context.action else "",
        resource_type=type(context.resource).__name__ if context.resource else "",
        resource_id=getattr(context.resource, "id", None),
        allowed=allowed,
        reason=reason or "",
        ip_address=ip_address,
    )

    logger.info(
        "Decisão de permissão: user_id=%s action=%s allowed=%s",
        getattr(context.user, "id", None),
        context.action.value if context.action else "",
        allowed,
    )

Boas práticas: segurança, escalabilidade e consistência

A regra mais importante é validar em múltiplas camadas, pois cada camada reduz um tipo de risco diferente. O isolamento por organização reduz vazamento de dados, políticas centralizam regras de negócio e checagens assíncronas tratam dependências externas. O princípio de falhar fechado evita que erros de integração virem permissões indevidas. O cache deve ser agressivo, mas com invalidação e TTLs bem definidos para não perpetuar decisões antigas.

Índices no banco precisam refletir os filtros usados, especialmente organization, created_at e created_by, pois isso afeta consultas frequentes. Para ações em lote, checagens podem ser agrupadas para evitar N+1, reduzindo chamadas repetidas. Em sistemas grandes, separar permissões de leitura e escrita melhora clareza e evita escaladas acidentais. Versionar políticas facilita evoluções sem quebrar contratos de autorização.

Conclusão: uma camada de autorização pronta para SaaS corporativo

Um sistema de permissões corporativo em Django se torna mais robusto quando combina isolamento por organização, políticas explícitas e validações assíncronas. A filtragem automática por tenant reduz o risco de consultas incorretas e garante um nível básico de proteção. O motor de políticas transforma regras de negócio em componentes reutilizáveis, testáveis e fáceis de manter. As checagens assíncronas permitem integrar serviços externos e controles como rate limiting sem comprometer a responsividade.

Com cache distribuído e auditoria, a autorização passa a ser também uma peça de performance e conformidade. O conjunto de modelos, managers, middleware, políticas, avaliador e integrações forma uma base completa para evoluir com novos requisitos. O resultado é uma arquitetura de autorização segura, consistente e adequada para aplicações SaaS com múltiplos tenants e regras complexas. Esse desenho mantém separação de responsabilidades e reduz mudanças perigosas em pontos dispersos do código.