Validate Tenant Isolation
Verifies tenant isolation is enforced at all layers (gateway, service, database).
Multi-Layer Validation
Reference: .cursorrules (Security Requirements), ModuleImplementationGuide.md Section 11
Layer 1: Gateway/API Layer
Check X-Tenant-ID header validation:
// ✅ Correct: Use authenticateRequest and tenantEnforcementMiddleware
import { authenticateRequest, tenantEnforcementMiddleware } from '@coder/shared';
fastify.get<{ Params: { id: string } }>(
'/api/v1/resource/:id',
{
preHandler: [authenticateRequest(), tenantEnforcementMiddleware()],
},
async (request, reply) => {
// ✅ tenantId automatically validated and attached by tenantEnforcementMiddleware
const tenantId = request.user!.tenantId;
const resource = await service.getResource(tenantId, request.params.id);
return reply.send({ data: resource });
}
);
Validation Checklist:
- • Routes use
authenticateRequest()andtenantEnforcementMiddleware()in preHandler - • Routes validate tenantId exists
- • Routes reject requests without X-Tenant-ID header
- • Tenant enforcement middleware registered
Layer 2: Service Layer
Check service methods require tenantId:
// ✅ Correct: tenantId is first parameter
class ResourceService {
async getResource(tenantId: string, id: string): Promise<Resource> {
// tenantId is required
}
async listResources(tenantId: string, filters: Filters): Promise<Resource[]> {
// tenantId is required
}
}
// ❌ Wrong: Missing tenantId
class ResourceService {
async getResource(id: string): Promise<Resource> {
// Missing tenantId
}
}
Validation Checklist:
- • All service methods have tenantId as first parameter
- • tenantId is validated (not empty, valid format)
- • Service methods never query without tenantId
Layer 3: Database Layer
Check all queries include tenantId in partition key:
// ✅ Correct: tenantId in WHERE clause
const query = `SELECT * FROM c WHERE c.tenantId = @tenantId AND c.id = @id`;
const parameters = [
{ name: '@tenantId', value: tenantId },
{ name: '@id', value: id }
];
// ❌ Wrong: No tenantId
const query = `SELECT * FROM c WHERE c.id = @id`;
Validation Checklist:
- • All queries include
c.tenantId = @tenantIdin WHERE clause - • tenantId is in partition key (first condition in WHERE)
- • All CREATE operations include tenantId in document
- • All UPDATE operations filter by tenantId first
- • All DELETE operations filter by tenantId first
Layer 4: Service-to-Service Communication
Check tenant propagation:
// ✅ Correct: Include X-Tenant-ID in service calls
const client = new ServiceClient({
baseURL: config.services.auth.url,
});
const response = await client.get('/api/v1/users/123', {
headers: {
'X-Tenant-ID': tenantId, // ✅ Propagate tenantId
'Authorization': `Bearer ${serviceToken}`,
},
});
Validation Checklist:
- • Service calls include X-Tenant-ID header
- • tenantId is extracted from request and propagated
- • No service calls without tenant context
Layer 5: Audit Logging
Check logs include tenantId:
// ✅ Correct: Include tenantId in logs
log.info('Resource created', {
resourceId: resource.id,
tenantId: tenantId, // ✅ Always include
userId: userId,
correlationId: requestId,
});
// ❌ Wrong: Missing tenantId
log.info('Resource created', {
resourceId: resource.id,
userId: userId,
});
Validation Checklist:
- • All log entries include tenantId
- • Error logs include tenantId
- • Audit logs include tenantId
- • Event logs include tenantId (organizationId field)
Validation Scripts
Check Database Queries
# Find queries without tenantId grep -r "SELECT.*FROM.*WHERE" src/ --exclude-dir=node_modules | grep -v "tenantId" # Find service methods without tenantId parameter grep -r "async.*\(.*\)" src/services/ --exclude-dir=node_modules | grep -v "tenantId"
Check Routes
# Find routes not using tenantEnforcementMiddleware grep -r "fastify\.(get|post|put|delete)" src/routes/ --exclude-dir=node_modules | grep -v "tenantEnforcementMiddleware"
Check Service Calls
# Find service calls without X-Tenant-ID grep -r "ServiceClient\|client\.(get|post|put|delete)" src/ --exclude-dir=node_modules | grep -v "X-Tenant-ID"
Comprehensive Checklist
Gateway/API Layer
- • All protected routes use
authenticateRequest()andtenantEnforcementMiddleware()in preHandler - • Routes access tenantId via
request.user!.tenantId - • Routes return 401 if X-Tenant-ID header missing (handled by middleware)
- • Tenant enforcement middleware used in all protected routes
Service Layer
- • All service methods have tenantId as first parameter
- • tenantId is validated (not empty, valid UUID format)
- • No service methods query without tenantId
Database Layer
- • All SELECT queries include
c.tenantId = @tenantId - • tenantId is first condition in WHERE clause (partition key)
- • All CREATE operations include tenantId in document
- • All UPDATE operations filter by tenantId before update
- • All DELETE operations filter by tenantId before delete
- • Container names use prefixed format:
{module-name}_data
Service Communication
- • All service calls include X-Tenant-ID header
- • tenantId is extracted and propagated to downstream services
- • No service calls made without tenant context
Logging
- • All log entries include tenantId
- • Error logs include tenantId
- • Audit logs include tenantId
- • Events include organizationId (tenantId) field
Events
- • All published events include organizationId (tenantId)
- • Event consumers validate tenantId before processing
Testing Tenant Isolation
Unit Tests
describe('tenant isolation', () => {
it('should not return resources from other tenants', async () => {
const tenantId = 'tenant-123';
const otherTenantId = 'tenant-456';
const result = await service.listResources(tenantId);
expect(result.every(r => r.tenantId === tenantId)).toBe(true);
expect(result.some(r => r.tenantId === otherTenantId)).toBe(false);
});
it('should throw error if tenantId is missing', async () => {
await expect(service.getResource('', 'resource-123')).rejects.toThrow();
});
});
Integration Tests
it('should return 400 without X-Tenant-ID header', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/v1/resource/resource-123',
headers: {
'Authorization': 'Bearer valid-token',
// Missing X-Tenant-ID
},
});
expect(response.statusCode).toBe(400);
});
Common Violations
- •
Missing tenantId in queries
- •Always include
c.tenantId = @tenantIdin WHERE clause
- •Always include
- •
Missing tenantId in service methods
- •tenantId should be first parameter
- •
Missing X-Tenant-ID in service calls
- •Always include in headers
- •
Missing tenantId in logs
- •Always include tenantId for traceability
- •
Missing tenantId in events
- •Always include organizationId field
Quick Validation
Run these checks before deployment:
- •No queries without tenantId: All database queries include tenantId
- •No routes without tenantId: All routes extract and validate tenantId
- •No service calls without tenantId: All service calls include X-Tenant-ID
- •No logs without tenantId: All logs include tenantId
- •No events without tenantId: All events include organizationId