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:
- Acoplado a HTTP context — não funciona em workers, jobs ou messaging handlers que rodam fora de uma request.
- 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.
- 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.