Pular para o conteúdo principal
Versão: main (dev) 🚧

ADR-004 — Role-based permission resolution

Data: 2026-05-01 Status: Aceito Contexto: DevPack 10.2.0, Bundle A

Contexto

O sistema de autorização do DevPack (introduzido na 2.0.0, área Authorization/) expõe IPermissionEvaluator como contrato para "o usuário corrente possui a permissão X?". O DevPack se declarou explicitamente agnóstico à origem das permissões — JWT claims, banco com cache, hierarquia de roles, etc., todas opções legítimas, decisão do consumidor.

A Fase 1 do template ITLab.Template.Next precisou de uma implementação default para que o DI resolvesse IPermissionEvaluator sem cada projeto criar uma. A Fase 1 entregou ClaimsPermissionEvaluator (no template, não no DevPack) que lia de IHttpContextAccessor.HttpContext.User.Claims procurando claims permission (custom) ou permissions (Microsoft Entra ID). Funcional, mas:

  1. Acoplado a HTTP context — não funciona em workers, jobs ou messaging handlers que rodam fora de uma request.
  2. Assume permissões diretamente em JWT — o que não é o padrão da maioria dos projetos enterprise da ITLab. JWT carrega roles; permissões são derivadas das roles via cache/banco.
  3. Vivia no template, não no DevPack — cada projeto novo teria que copiar.

A 10.2.0 é a oportunidade de mover o caminho default para o DevPack e alinhar com o padrão real ("permissões via roles") sem fechar a porta para implementações customizadas.

Decisão

Adicionar dois novos tipos no namespace Authorization/:

IRolePermissionResolver — contrato

public interface IRolePermissionResolver
{
Task<IReadOnlyCollection<string>> ResolvePermissionsAsync(
IEnumerable<string> roles,
CancellationToken cancellationToken = default);
}

Implementação fica no projeto consumidor — tipicamente uma classe que consulta tabela RolePermission com cache em memória ou Redis. O DevPack não opina sobre a fonte ou a estratégia de cache.

RoleBasedPermissionEvaluator — implementação default

public sealed class RoleBasedPermissionEvaluator(
ICurrentUser currentUser,
IRolePermissionResolver resolver) : IPermissionEvaluator
{
private IReadOnlyCollection<string>? _cachedPermissions;
// HasPermission/HasAny/HasAll resolvem via ICurrentUser.UserRoles + resolver
// Cache de campo deduplica chamadas dentro do mesmo escopo.
}
  • Lifetime: Scoped. Uma instância por request HTTP. Cache de campo é seguro porque o escopo termina junto com a request.
  • Cache por-instância: primeira chamada a HasPermissionAsync/etc. dispara o resolver; chamadas subsequentes na mesma request reusam o resultado. Garante que múltiplos [RequiresPermission] em um pipeline de mediator não geram N consultas ao banco/cache.

AddDevPackRoleBasedPermissionEvaluator<TResolver>() — registration

public static IServiceCollection AddDevPackRoleBasedPermissionEvaluator<TResolver>(
this IServiceCollection services)
where TResolver : class, IRolePermissionResolver
{
services.AddScoped<IRolePermissionResolver, TResolver>();
services.AddScoped<IPermissionEvaluator, RoleBasedPermissionEvaluator>();
return services;
}

Vincula o evaluator + um resolver concreto em uma chamada. services.AddDevPackAuthorization() permanece intacto — registra policy provider + handler + behavior. As duas extensões são compostas no Program.cs quando o consumidor escolhe o caminho role-based.

Filosofia preservada: ICurrentUser permanece sem Permissions

Considerou-se adicionar IReadOnlyCollection<string>? Permissions em ICurrentUser e popular num middleware. Descartado:

  • Acoplaria a abstração de identidade a uma fonte específica (claims OU resolver), quebrando a generalidade.
  • Forçaria resolução em toda request, mesmo endpoints anônimos ou que não fazem authorization check.
  • Permissões são dado derivado de roles + contexto, não identidade. Manter separados respeita a separação de concerns.

ICurrentUser continua expondo UserRoles (identidade) e LocalTimeZone (preferência), sem Permissions (derivado).

Alternativas consideradas

ClaimsPermissionEvaluator (Fase 1 do template) movido para o DevPack

Descartado. Acoplado a IHttpContextAccessor e assume formato de claim que não é universal. Útil para projetos que efetivamente colocam permissões no JWT, mas restringe demais para virar default. Pode ser adicionado como capability opcional em release futura se houver demanda.

Permissões direto no JWT (sem resolver)

Descartado. O projeto usa Microsoft Entra ID como IdP e gera JWT interno com roles. Colocar permissões no JWT exigiria:

  • Regenerar o token a cada mudança de permissão (UX ruim).
  • Aumentar tamanho do token significativamente em projetos com 50+ permissões.
  • Replicar a lógica de cache/expiração do banco no token (overhead).

Resolver server-side com cache é mais flexível e econômico.

IPermissionResolver em vez de IRolePermissionResolver

Considerou-se um nome mais genérico (resolver de permissões, sem mencionar roles). Descartado porque o nome IRolePermissionResolver documenta o caminho default explicitamente. Quem precisa de algo mais genérico implementa IPermissionEvaluator diretamente — sem passar pelo resolver.

Consequências

  • Adição não-quebrante. IPermissionEvaluator permanece como contrato; nenhum consumidor existente é afetado. Quem já tem implementação custom continua usando ela.
  • O template ITLab.Template.Next (Fase 1) substituirá ClaimsPermissionEvaluator por RoleBasedPermissionEvaluator quando consumir DevPack 10.2.0 — pendência registrada no relatório consolidado da release.
  • Projetos consumidores devem implementar IRolePermissionResolver próprio. Tipicamente: classe DbRolePermissionResolver que consulta RolePermission com IMemoryCache/Redis.
  • 9 testes novos em RoleBasedPermissionEvaluatorTests cobrindo: hit, miss, null roles, empty roles, HasAny±, HasAll±, e validação de cache por-instância (resolver chamado Times.Once para 3 operações distintas — cobre o cenário-mãe de múltiplos [RequiresPermission] na mesma request).

Referências