86 lines
2.7 KiB
Markdown
86 lines
2.7 KiB
Markdown
# Tenant Resolution
|
|
|
|
## 1. Goal
|
|
Resolve tenant from request domain and enforce strict tenant isolation for all config and business endpoints.
|
|
|
|
## 2. Middleware Flow
|
|
1. Parse request host (`Host`/`X-Forwarded-Host` as trusted by ingress policy).
|
|
2. Normalize host (lowercase, strip port, normalize punycode if needed).
|
|
3. Resolve tenant record from host mapping store.
|
|
4. Validate tenant status (`active`, not suspended/expired).
|
|
5. Attach tenant context to request.
|
|
6. Continue to route handlers with tenant-scoped services.
|
|
|
|
## 3. Request Context Attachment
|
|
Attach immutable context object, e.g.:
|
|
- `req.ctx.tenantId`
|
|
- `req.ctx.tenantSlug`
|
|
- `req.ctx.host`
|
|
- `req.ctx.defaultLocale`
|
|
- `req.ctx.allowedLocales`
|
|
|
|
All downstream services must read tenant from context, not from query params.
|
|
|
|
## 4. Pseudocode Middleware Example
|
|
```ts
|
|
async function tenantResolver(req, res, next) {
|
|
const host = normalizeHost(req.headers['x-forwarded-host'] || req.headers.host);
|
|
if (!host) return res.status(400).json({ error: 'INVALID_HOST' });
|
|
|
|
const cacheKey = `tenant:host:${host}`;
|
|
let tenant = await cache.get(cacheKey);
|
|
|
|
if (!tenant) {
|
|
tenant = await tenantRepository.findByHost(host);
|
|
if (tenant) await cache.set(cacheKey, tenant, { ttlSeconds: 300 });
|
|
}
|
|
|
|
if (!tenant) return res.status(404).json({ error: 'TENANT_NOT_FOUND' });
|
|
if (!tenant.active) return res.status(403).json({ error: 'TENANT_INACTIVE' });
|
|
|
|
req.ctx = {
|
|
...(req.ctx || {}),
|
|
tenantId: tenant.id,
|
|
tenantSlug: tenant.slug,
|
|
host,
|
|
defaultLocale: tenant.defaultLocale,
|
|
allowedLocales: tenant.supportedLocales
|
|
};
|
|
|
|
return next();
|
|
}
|
|
```
|
|
|
|
## 5. Caching Strategy
|
|
Recommended:
|
|
- L1 in-memory cache per API instance for hot host lookups.
|
|
- L2 distributed cache (Redis) for cross-instance consistency.
|
|
- Cache key by host.
|
|
- Short TTL (60-300s) + explicit invalidation on tenant changes.
|
|
|
|
Do not cache authorization decisions globally; only cache tenant mapping metadata.
|
|
|
|
## 6. Security Rules
|
|
- Never trust tenant from client payload.
|
|
- Always derive tenant from validated host/context.
|
|
- Enforce tenant filter at repository layer for every query.
|
|
- Reject cross-tenant IDs even if resource exists globally.
|
|
- Emit audit logs for tenant mismatch attempts.
|
|
|
|
## 7. Edge Cases
|
|
### Unknown Domain
|
|
- Behavior: return `404 TENANT_NOT_FOUND`.
|
|
- Optional: redirect only if explicit global fallback policy exists.
|
|
|
|
### Inactive Tenant
|
|
- Behavior: return `403 TENANT_INACTIVE`.
|
|
- Optional: include support contact metadata in response.
|
|
|
|
### Host Header Poisoning
|
|
- Use trusted proxy chain rules.
|
|
- Ignore untrusted forwarded host headers.
|
|
|
|
### Local Development Domains
|
|
- Keep explicit local host mapping table.
|
|
- No projectName shortcuts for tenant selection.
|