ADR-004 — DB-backed role/permission resolution with IMemoryCache
Date: 2026-05-08 Status: Accepted
Context
A Fase 2 shipou InMemoryRolePermissionResolver — três roles hard-coded (Admin/Operator/Viewer) com permissões fixas no código. Comentários no DI explicitavam "TEMPLATE PLACEHOLDER — replace before production".
Custos do placeholder:
- Cada consumidor reescreve antes do primeiro deploy — mas há padrões comuns (tabela + cache) que poderiam viver no template.
- Cobertura de testes do path de autorização ficava amarrada às 3 roles fixas.
- Mudanças em roles exigiam deploy de código.
Patterns considerados para produção:
- DB-backed — tabela
RolePermissions(RoleName, Permission)com cache. - Config-driven InMemory —
appsettings:Authorization:Roleslido porIOptionsMonitor, hot reload. - Identity Provider claims — role/permission claims emitidos pelo IdP.
Decision
DB-backed resolver com IMemoryCache em frente, shipado canon. Estrutura:
- Entidade
RolePermissionemInfrastructure/Authentication/(composite PKRoleName + Permission, sem invariantes de domínio — pure infra). RolePermissionConfigurationmapeia a tabela e usaHasDatapara seed dos 3 roles canônicos (Admin/Operator/Viewer×Orders.*). HasData funciona tanto em migrations relacionais quanto em EF Core InMemory (viaEnsureCreated), unificando dev + CI + prod.DbRolePermissionResolverlê por role comIMemoryCache(TTL 5 min). Hot path: 1 query por miss + lookups O(1).- DI:
AddMemoryCache()+AddDevPackRoleBasedPermissionEvaluator<DbRolePermissionResolver>(). AppDbContextexpõeDbSet<RolePermission> RolePermissionsmas não o expõe viaIAppDbContext— handlers de feature não devem queryar essa tabela diretamente; só o resolver lê.
Estender o catálogo:
- Adicionar constante em
Permissions.cs. - Estender
RolePermissionConfiguration.HasDatacom novas linhas. dotnet ef migrations add Add{NewRole}.
Multi-instance (Redis):
- Swap
IMemoryCacheporIDistributedCacheno resolver — mesma superfície.
Consequences
Positivas:
- Template funcional out-of-the-box: rodar
dotnet ef database update+ ter usuário com claim de role já basta. - Padrão único para todos os consumidores — menos drift entre projetos.
- Tests integration exercitam o caminho real (DbRolePermissionResolver + HasData seed via EnsureCreated).
- Cache reduz pressão sobre DB; TTL 5min é trade-off razoável entre frescor e custo.
Negativas:
- Adiciona uma tabela extra (
RolePermissions) ao schema mínimo. Consumidores com permissões 100% IdP-driven precisam remover/ignorar. - Cache local (
IMemoryCache) em multi-instance fica até 5 min divergente após mudança —IDistributedCacheresolve mas exige Redis. - Mudança de role exige migration ou edição direta no DB.
Alternatives considered
| Opção | Por que descartada |
|---|---|
Manter InMemoryRolePermissionResolver placeholder | Cada consumidor reescreve; padrão fica perdido. |
| Config-driven InMemory (appsettings) | Hot reload é vantagem pequena; arquivo de config cresce sem governança. |
| Claims do IdP (sem tabela local) | Acopla auth à definição de permissions — mudanças exigem coordenação com o IdP team. Não impede uso futuro com claims (consumidor sobrescreve IRolePermissionResolver). |
References
CLAUDE.md§ Role catalogue (RolePermissiontable).src/.../Infrastructure/Authentication/DbRolePermissionResolver.cs.src/.../Infrastructure/Persistence/Configurations/RolePermissionConfiguration.cs.- DevPack ADR-004 (role-based permission resolution surface, originou daqui).