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

ADR-012 — Per-tenant authentication settings (substrate; local provider only for now)

Date: 2026-06-23 Status: Accepted Extends: ADR-006 (local Identity dual-scheme), ADR-009, ADR-011 (pre-auth domain resolution). Pairs with: DevPack ADR-008 (settings store + secret protection). Reference: ITLab.WorkGuardian (TenantSetting/BaseSettings, public auth-config query), adapted to the canon.

Context

The roadmap's Phase 3 lets each tenant choose its authentication provider(s) — local, Microsoft Entra ID, LDAP — with its own config/secrets, discovered by the SPA before login. This ADR records the substrate delivered now; per the ratified scope, only the local provider (DefaultProvider) is wired — Entra/LDAP scheme wiring and the SPA/MSAL work are deferred. The foundation is built so that work slots in without a breaking change.

Builds on DevPack 10.7.1, which ships the universal pieces (DevPack ADR-008): ISecretProtector (Data Protection) and the IPerTenantSettingsStore contract.

Decision

1. TenantSetting is IMultiTenant

Domain/Tenants/TenantSetting.cs (AuditedSoftDeletableEntity<int>, IMultiTenant): Key, Value (string), Type (SettingType enum: String/Boolean/Integer/Protected), IsSecret (= Protected). Unlike Tenant/TenantUser, this is tenant-scoped data, so it is IMultiTenant — the read filter scopes it and the write interceptor guards cross-tenant modify/delete. TenantSettingConfiguration adds a unique filtered index on (TenantId, Key); no manual query filter (the applicator owns it). TenantSettingKey holds the dotted keys (AuthProvider.{DefaultProvider,EntraID,Ldap}).

2. EF IPerTenantSettingsStore impl with store-side encryption + cache

Infrastructure/Identity/PerTenantSettingsStore.cs implements the DevPack contract over TenantSetting. Secret values are encrypted via ISecretProtector in SetAsync(mustProtect) and decrypted in the getters (callers always see plaintext). Reads use IgnoreQueryFilters() + explicit TenantId so the anonymous public-config endpoint (no current tenant) works. The full per-tenant map is cached in IMemoryCache for 5 minutes and evicted on write — the same shape as DbRolePermissionResolver.

3. Slices + endpoints

  • GetPublicAuthConfiganonymous (GET /v1/auth/config); resolves the tenant by host (ADR-011) and returns enabled providers + primary, no secrets. Falls back to local-only when domain validation is off or the host does not resolve, so login is never blocked.
  • GetAuthenticationSettings / UpdateAuthenticationSettings — admin (GET/PUT /v1/tenants/{id}/auth-settings), gated by Tenants.AuthSettings.Read / Tenants.AuthSettings.Update (seeded for Admin). Reuses the DevPack TenantAuthenticationSettingsDto. The update validator forbids disabling the local provider while it is the only wired one.

4. Seed + migration

IdentitySeeder seeds the default tenant's provider flags (local enabled; Entra/LDAP disabled) idempotently via the store. The AddTenantSettings migration creates the table + the two new RolePermission rows.

5. No scheme wiring, no SPA (deferred)

Program.cs is unchanged — no Entra/LDAP schemes yet. The "static schemes + per-tenant validation in OnTokenValidated" recipe (DevPack ADR-008) is the path when those providers land; IPerTenantSettingsStore is the seam they'll read config from.

Alternatives considered

  • TenantSetting not IMultiTenant (like TenantUser): rejected — settings are genuinely tenant-scoped data; IMultiTenant gives the read filter + write guard for free. The store reads cross-tenant via IgnoreQueryFilters where needed (public config).
  • Encryption via a SaveChangesInterceptor (WG mirror): rejected at the DevPack level (ADR-008) — store-side encryption avoids double-encryption; the template follows.
  • Add Entra/LDAP config + secrets now: deferred per ratified scope. The substrate (Protected type, mustProtect, secret round-trip) is in place and tested so it's a non-breaking add later.
  • Expose TenantSettings raw to handlers: avoided — handlers go through the store so secret decryption + caching are centralized (the store is the settings access pattern, not a generic repository).

Consequences

  • Non-breaking and opt-in: with no settings seeded and domain validation off, /v1/auth/config returns local-only; existing auth is unchanged.
  • Secret-at-rest works today (tested) even though no provider currently stores a secret — ready for Entra/LDAP.
  • Bumps DevPack 10.6.0 → 10.7.1.
  • Tests: AuthSettingsTests (public config; admin update→get round-trip; disabling-only-provider rejected; missing-permission 403; secret encrypted at rest + decrypted on read) + TenantSettingTests.
  • InMemory caveat: the InMemory provider does not enforce the (TenantId, Key) unique index; uniqueness is exercised against SQL Server.

References

  • DevPack ADR-008, ADR-011.
  • Domain/Tenants/TenantSetting.cs, Persistence/Configurations/TenantSettingConfiguration.cs, Persistence/Migrations/*_AddTenantSettings.cs.
  • Infrastructure/Identity/PerTenantSettingsStore.cs, Application/Tenants/AuthSettings/*, Api/Endpoints/{AuthEndpoints,TenantsEndpoints}.cs.