validate-tenant-isolation
Verifies tenant isolation is enforced at all layers (gateway, service, database) following .cursorrules Security Requirements and ModuleImplementationGuide.md Section 11. Checks X-Tenant-ID header validation in routes, verifies tenantId in all database queries, validates tenant enforcement middleware, checks service-to-service tenant propagation, verifies audit logging includes tenantId, and ensures tenantId is in partition key for all Cosmos DB queries. Use when performing security audits, pre-deployment checks, or ensuring multi-tenancy compliance.
SKILL.md
| Name | validate-tenant-isolation |
| Description | Verifies tenant isolation is enforced at all layers (gateway, service, database) following .cursorrules Security Requirements and ModuleImplementationGuide.md Section 11. Checks X-Tenant-ID header validation in routes, verifies tenantId in all database queries, validates tenant enforcement middleware, checks service-to-service tenant propagation, verifies audit logging includes tenantId, and ensures tenantId is in partition key for all Cosmos DB queries. Use when performing security audits, pre-deployment checks, or ensuring multi-tenancy compliance. |
name: validate-tenant-isolation description: Verifies tenant isolation is enforced at all layers (gateway, service, database) following .cursorrules Security Requirements and ModuleImplementationGuide.md Section 11. Checks X-Tenant-ID header validation in routes, verifies tenantId in all database queries, validates tenant enforcement middleware, checks service-to-service tenant propagation, verifies audit logging includes tenantId, and ensures tenantId is in partition key for all Cosmos DB queries. Use when performing security audits, pre-deployment checks, or ensuring multi-tenancy compliance.
Validate Tenant Isolation
Verifies tenant isolation is enforced at all layers (gateway, service, database).
Tenant-only: Use tenantId only; there is no organization. All users and data are scoped by tenant. Do not use or validate organizationId; APIs, events, and database partition keys use tenantId only.
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
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 tenantId field
Events
- All published events include 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
-
Using organizationId
- Use
tenantIdonly; there is no organization. Replace anyorganizationIdwithtenantId.
- Use
-
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 tenantId 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 tenantId