ADR-008 — Per-tenant settings store + secret protection (foundation for per-tenant auth)
Data: 2026-06-22
Status: Aceito
Contexto: DevPack 10.7.1 (a 10.7.0 saiu por engano sem este conteúdo)
Pareado com: ADR-012 do template ITLab.Template.Next (consumo no canon) e ADR-005 (Identity local — base da auth).
Base de referência: padrão de settings por-tenant + proteção de segredo portado de ITLab.WorkGuardian (TenantSetting/BaseSettings, ProtectTenantSettingsInterceptor, IDataProtectionService).
Contexto
A Fase 3 do roadmap de multi-tenancy quer que cada tenant escolha/configure seu provider de auth (local, e no futuro Entra ID/LDAP) com credenciais próprias. Isso exige um lugar para guardar config por-tenant — incluindo segredos (client secret, bind password) que precisam ser cifrados em repouso — e um contrato para lê-la, inclusive fora de um handler Mediator (ex.: um evento OnTokenValidated por-tenant). O plumbing é universal a qualquer consumidor multi-tenant (G-01) → DevPack.
O escopo desta entrega é só o substrato (o consumidor — template — habilita apenas o provider local nesta fase; Entra/LDAP ficam para depois). Mesmo assim o contrato precisa ser completo e à prova de futuro para os providers externos não exigirem mudança quebrante.
Decisão
1. Abstractions/ISecretProtector + DataProtectionSecretProtector
Contrato simétrico Protect/Unprotect para cifrar segredos em repouso. Impl default sobre ASP.NET Core Data Protection (IDataProtectionProvider.CreateProtector(purpose)), no shared framework — sem pacote novo. DI: AddDevPackSecretProtector() (registra AddDataProtection() + ISecretProtector via TryAdd, então o consumidor pode trocar a impl). O gerenciamento de chave (storage/rotação) é do host; em produção exige key ring persistente (doc).
2. Identity/Settings/IPerTenantSettingsStore (só a interface)
GetAsync(tenantId, key), GetByPrefixAsync(tenantId, prefixes), SetAsync(tenantId, key, value, mustProtect), InvalidateCache(tenantId). String-valued (settings serializam para string; o caller parseia) para o contrato não embutir opinião de serialização. A impl é do consumidor (EF sobre a entidade de settings) — interface no DevPack porque é universal e é o seam que código de auth lê config de fora de um handler.
3. Cifragem no store, não num interceptor
O WorkGuardian cifra via um SaveChangesInterceptor (ProtectTenantSettingsInterceptor) que varre TenantSetting no save. Descartado aqui: o interceptor reintroduz o risco de dupla cifragem (um Modified que não tocou o valor re-cifra o ciphertext) e exige a entidade expor estado de "já protegido". Como toda escrita passa pelo store, cifrar dentro de SetAsync(mustProtect) (e decifrar em GetAsync) cifra exatamente uma vez, sem flag de estado, sem reflexão sobre nome de propriedade. O plano marcava o interceptor como "Avaliar" (opcional) — avaliado e dispensado.
4. Schemes estáticos + validação dinâmica nos eventos (recipe, não abstração)
A restrição central do ASP.NET Core — schemes são estáticos (registrados na startup) — fica documentada como recipe em Identity/Settings/README.md: registre schemes fixos e aplique a config por-tenant nos eventos (OnTokenValidated), lendo IPerTenantSettingsStore. Não promovemos IPerTenantAuthSchemeResolver a abstração agora (decisão ratificada) — sem provider externo nesta fase, abstrair seria prematuro; promove-se quando o reuso aparecer.
Alternativas consideradas
- Interceptor de proteção (mirror do WG): descartado — dupla cifragem + estado de entidade; store-side é mais simples e correto (ver Decisão 3).
- Store tipado
Get<T>: descartado por ora — string-valued mantém o contrato sem opinião de serialização; o consumidor parseia"true"/"42". IPerTenantAuthSchemeResolverabstrato já: descartado — recipe primeiro; sem Entra/LDAP nesta fase não há reuso que justifique.- Cripto própria (AES como o WG
DataProtectionService): descartado — Data Protection do framework já resolve key management/rotação; reimplementar AES+IV fixo é pior.
Consequências
- Adição não-quebrante: tipos novos, nada muda.
AddDevPackSecretProtector()é opt-in. - Primeiro consumidor: template
ITLab.Template.Next(ADR-012) —TenantSetting+ impl EF do store + slices de auth-config (sóDefaultProviderhabilitado nesta fase). - Pronto para Entra/LDAP: quando vierem, registram schemes estáticos e leem segredos via o store (a recipe já está documentada); o contrato não muda.
- Testes:
SecretProtectorTests(round-trip, token adulterado lança, tokens não-intercambiáveis entre key rings).
Referências
- ADR-001 — No framework wrappers. ADR-005 — Identity local.
Abstractions/ISecretProtector.cs,Abstractions/DataProtectionSecretProtector.csIdentity/Settings/IPerTenantSettingsStore.cs+Identity/Settings/README.md