ADR-007 — Domain tenant resolution + glue middleware
Data: 2026-06-22
Status: Aceito
Contexto: DevPack 10.6.0
Pareado com: ADR-011 do template ITLab.Template.Next (consumo no canon) e ADR-006 (persistência claim-based)
Base de referência: contributors + middleware portados de ITLab.WorkGuardian (estilo ABP)
Contexto
A 10.4.0/10.5.0 entregou as abstrações de resolução (ITenantResolver, ITenantResolveContributor, TenantResolveContext/Options/Result, ITenantAccessValidator) e a persistência claim-based (ADR-006), mas o pipeline de resolução era scaffolding dormente: nenhum contributor concreto, nenhuma glue middleware, nenhuma forma de resolver o tenant pelo host. Qualquer consumidor que quisesse acme.app.com → tenant "acme" teria que reescrever a extração de host, o matching por padrão e a middleware de validação — plumbing universal (G-01) que pertence ao DevPack.
ITLab.WorkGuardian já tinha esse plumbing maduro (HttpTenantResolveContributorBase, DomainTenantResolveContributor, MultiTenancyOptionsExtensions, TenantValidationMiddleware). As abstrações do DevPack são idênticas em assinatura às do WorkGuardian (Task-based, mesmos membros), então o port é cópia + troca de namespace, sem adapter.
Decisão
Adicionar o caminho de domínio como peça universal, opt-in por config, mantendo claim-based como default.
1. Contributors (Identity/MultiTenancy/)
HttpTenantResolveContributorBase— base para contributors que leem oHttpContext(resolvido viaITenantResolveContext.GetHttpContext(), que requerIHttpContextAccessor). Captura exceções e loga, para um contributor nunca quebrar o pipeline.DomainTenantResolveContributor— extrai o host na ordemOrigin→Referer→Request.Host, remove o prefixo de protocolo, e casa contraAlternateDomains(overrides literais host→tenant, primeiro) eDomainFormats(padrões com placeholder viaFormattedStringValueExtracter). Logger opcional para diagnóstico.MultiTenancyOptionsExtensions.AddDomainTenantResolver(formats, alternateDomains, serviceProvider?)— insere o contributor no início da lista (Insert(0, …)), para resolução por host preceder estratégias posteriores.
2. Options (AppSettings/)
MultiTenancyOptions(IAppOptions, seçãoMultiTenancyOptions):EnableDomainValidation(default false),DefaultDevelopmentTenant,DomainFormats,AlternateDomains,ExcludedPaths.AlternateDomainOption:Host+Tenant.
Reduzido em relação ao WorkGuardian: removido EnableMiddlewareValidation (gate redundante — EnableDomainValidation sozinho governa a middleware) e os ExcludedPaths default específicos de OAuth do WG (DevPack ship lista vazia; o consumidor configura).
3. Glue middleware no DevPack, não no consumidor
Middleware/TenantResolutionMiddleware (app.UseDevPackTenantResolution()) amarra o fluxo: gate em EnableDomainValidation → skip anônimo/ExcludedPaths → ITenantResolver.ResolveTenantAsync() → ITenantAccessValidator.GetTenantIdByDomainNameAsync → ValidateAccessAsync → cross-check do id resolvido contra ICurrentUser.TenantId.
Decisão G-01: o WorkGuardian mantém a middleware no app, mas o fluxo é idêntico para qualquer consumidor que use as interfaces. Portá-la ao DevPack (parametrizada por MultiTenancyOptions + ITenantAccessValidator) deixa o consumidor só implementando o access validator e ligando a config. O acoplamento ao app é zero: o tenant do token vem de ICurrentUser (abstração do próprio DevPack), não de um claim hardcoded.
4. Cross-check via ICurrentUser, não re-lendo o claim
A middleware do WorkGuardian relê o claim Tenant (GlobalConsts.TenantClaim) e chama context.User.GetUserId(). A do DevPack usa ICurrentUser.TenantId/ICurrentUser.Id — já populados pelo bridge (UseDevPackIdentityContext, ADR-006). Isso (a) remove o acoplamento ao nome do claim (o bridge lê tenant/tid, mas a middleware não precisa saber) e (b) força a ordem correta: a middleware tem que rodar depois do bridge, o que o XML doc e o README deixam explícito.
5. Status codes
401 quando o token não traz tenant, o host não resolve, ou há divergência host-vs-token; 404 quando o host mapeia para nenhum tenant; 403 quando o usuário autenticado não tem membership no tenant resolvido. (WorkGuardian usava 401 também para "sem acesso"; 403 é semanticamente mais correto e alinha com o TenantAccessDenied → 403 do template.)
Alternativas consideradas
- Glue middleware no template (como no WorkGuardian): descartada — o fluxo é universal; manter no template duplicaria em todo consumidor. O cross-check via
ICurrentUserelimina o único acoplamento que justificaria mantê-la no app. - Re-ler o claim de tenant na middleware: descartado — acopla ao nome do claim e à ordem implicitamente;
ICurrentUsertorna a dependência explícita. EnableMiddlewareValidationseparado deEnableDomainValidation: descartado — dois gates para o mesmo efeito; um só (EnableDomainValidation) é suficiente.- Default
EnableDomainValidation = true: descartado — quebraria o caminho claim-only de consumidores existentes e o probedotnet new(sem subdomínios). Opt-in é o default seguro.
Consequências
- Adição não-quebrante. Nenhum tipo existente muda. Com
EnableDomainValidation = false(default) a middleware é no-op e a tenancy continua claim-based — consumidores da 10.5.0 não percebem. - Requer
IHttpContextAccessorregistrado quando o domínio é ativado (DomainTenantResolveContributorresolve o request por ele). - Primeiro consumidor: template
ITLab.Template.Next(ADR-011) —Tenant.DomainName,TenantAccessValidator.GetTenantIdByDomainNameAsync, config opt-in. - Testes novos:
DomainTenantResolveContributorTests(host/Origin/Referer, alternate domain, no-match, sem HttpContext) +TenantResolutionMiddlewareTests(disabled/anonymous/excluded passam; missing-token-tenant/unresolved/mismatch → 401; not-found → 404; no-membership → 403; match → next).
Referências
- ADR-001 — No framework wrappers (contributors/middleware somam-se sem embrulhar ASP.NET Core).
- ADR-006 — Persistência claim-based (o bridge que popula
ICurrentUser.TenantId). Identity/MultiTenancy/DomainTenantResolveContributor.csIdentity/MultiTenancy/HttpTenantResolveContributorBase.csIdentity/MultiTenancy/MultiTenancyOptionsExtensions.csMiddleware/TenantResolutionMiddleware.csAppSettings/MultiTenancyOptions.cs