This commit is contained in:
85
docs/backend-platform/tenant-resolution.md
Normal file
85
docs/backend-platform/tenant-resolution.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user