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.