JMJaedson Barbosa Macedo
Todos os posts

Autorização faz parte do domínio, não do controller

3 min de leitura
  • backend
  • domínio
  • autorização
  • arquitetura

Quando um sistema começa pequeno, a autorização quase sempre nasce como um if no controller: o usuário está logado? é dono do recurso? tem o papel certo? À medida que o domínio cresce, esses ifs se multiplicam, se contradizem e passam a viver longe das regras de negócio que deveriam proteger.

Este post é sobre um princípio simples: quem pode fazer o quê é uma regra de domínio, e regras de domínio pertencem ao modelo — não à borda HTTP.

O problema do if espalhado

Considere um endpoint que arquiva um projeto. A primeira versão costuma ser assim:

projects.controller.ts
async function archiveProject(req: Request, res: Response) {
  const project = await projects.findById(req.params.id);
  if (!project) return res.status(404).end();
 
  // Regra de autorização perdida no meio do controller.
  if (project.ownerId !== req.user.id && req.user.role !== "admin") {
    return res.status(403).end();
  }
 
  project.archivedAt = new Date();
  await projects.save(project);
  res.json(project);
}

O problema não é o código em si — é onde ele mora. Essa mesma regra ("dono ou admin") precisa ser repetida em cada endpoint que mexe no projeto: editar, excluir, convidar membros. Uma hora alguém esquece um dos lugares, e você tem uma falha de autorização silenciosa.

Movendo a decisão para o domínio

A pergunta "este usuário pode arquivar este projeto?" só depende de dois objetos do domínio: o usuário e o projeto. Ela não precisa saber nada sobre HTTP, req ou res. Então ela pertence ao domínio:

project.ts
class Project {
  constructor(
    readonly id: string,
    readonly ownerId: string,
    private archivedAt: Date | null,
  ) {}
 
  canBeArchivedBy(user: User): boolean {
    return this.ownerId === user.id || user.isAdmin();
  }
 
  archive(user: User): void {
    if (!this.canBeArchivedBy(user)) {
      throw new ForbiddenError("Sem permissão para arquivar este projeto.");
    }
    this.archivedAt = new Date();
  }
}

Agora o controller volta a fazer só o trabalho dele — traduzir HTTP para domínio e de volta:

projects.controller.ts
async function archiveProject(req: Request, res: Response) {
  const project = await projects.findById(req.params.id);
  if (!project) return res.status(404).end();
 
  project.archive(req.user); // a regra vive aqui dentro
  await projects.save(project);
  res.json(project);
}

A borda HTTP não decide se algo pode acontecer. Ela só pergunta ao domínio e converte a resposta em um status code.

Por que isso importa

Mover a autorização para o modelo traz três ganhos concretos:

  • Um único lugar para a regra. Toda operação sobre o projeto passa por archive/canBeArchivedBy. Não há como esquecer de checar em um endpoint novo — o método simplesmente não deixa.
  • Testável sem HTTP. Você testa canBeArchivedBy com objetos puros, sem subir um servidor nem simular req/res.
  • Lê como o negócio fala. project.archive(user) descreve a intenção; if (project.ownerId !== req.user.id && ...) descreve mecânica.

Onde parar

Isso não quer dizer jogar toda autorização no modelo. Regras transversais — "a conta está suspensa?", "o token expirou?" — são melhores como middleware, antes de chegar no domínio. A distinção que uso:

Tipo de regraOnde vive
Depende de quem é o usuárioMiddleware
Depende do estado de uma entidadeModelo de domínio

Autorização baseada em estado do domínio ("dono do projeto", "membro da equipe", "autor do comentário") pertence ao domínio. Autorização baseada em identidade ("autenticado", "não banido") pertence à borda.

A regra prática: se responder à pergunta exige carregar uma entidade e olhar seus campos, a decisão mora junto dessa entidade.