| name | test-multitenant-isolation |
| description | Validates complete data isolation between tenants, tests cross-tenant access, ensures proper query scoping |
| user-invocable | true |
Multi-Tenant Isolation Testing Skill
Purpose
Validates complete data isolation between tenants to prevent catastrophic data leaks. Tests cross-tenant access attempts and ensures all database queries are properly scoped.
CLAUDE.md Compliance
- ✅ Tests use synthetic data (no external dependencies)
- ✅ Deterministic test execution
- ✅ Tests both success and attack scenarios
- ✅ Validates security-critical functionality
Usage
Run this skill:
- After authentication/authorization changes
- After database schema modifications
- Before production deployments
- After tenant-related code changes
- Weekly security audits
Prerequisites
- SQLite or PostgreSQL database
- Test database cleanup (automatic via tempfile)
Commands
Comprehensive Multi-Tenant Test
cargo test --test mcp_multitenant_complete_test --features testing -- --nocapture
Quick Isolation Check
cargo test multitenant -- --nocapture
cargo test test_cross_tenant -- --nocapture
cargo test test_tenant_middleware -- --nocapture
Database Query Scoping Validation
echo "🔍 Checking for unscoped queries..."
rg "SELECT.*FROM.*WHERE" src/ --type rust -A 3 | rg -v "tenant_id" | head -20
echo "🔍 Checking route handler tenant context..."
rg "Extension.*TenantContext" src/routes/ --type rust -n | wc -l
rg -i "tenant.*=.*\"[a-f0-9-]{36}\"" src/ --type rust -n || echo "✓ No hardcoded tenant IDs"
Test Scenarios
Scenario 1: Cross-Tenant Activity Access
Scenario 2: Tenant Context Injection
Scenario 3: API Key Isolation
Scenario 4: OAuth Token Isolation
Security Checks
Database Query Patterns
SELECT * FROM activities WHERE tenant_id = $1 AND user_id = $2
INSERT INTO activities (tenant_id, user_id, ...) VALUES ($1, $2, ...)
UPDATE activities SET ... WHERE tenant_id = $1 AND id = $2
DELETE FROM activities WHERE tenant_id = $1 AND id = $2
TenantContext Pattern
pub async fn get_activities(
Extension(tenant): Extension<TenantContext>,
Json(params): Json<GetActivitiesParams>,
) -> Result<Json<Activities>, AppError> {
}
pub async fn get_activities(
Json(params): Json<GetActivitiesParams>,
) -> Result<Json<Activities>, AppError> {
}
Test Output Analysis
Expected Output
test test_tenant_isolation ... ok
test test_cross_tenant_activity_access ... ok (should fail access)
test test_cross_tenant_user_access ... ok (should fail access)
test test_tenant_oauth_isolation ... ok
test test_tenant_api_key_isolation ... ok
test result: ok. 12 passed; 0 failed
Failure Indicators
# ❌ BAD: Cross-tenant access succeeded
test test_cross_tenant_activity_access ... FAILED
Expected: Forbidden or empty
Actual: Returned data from other tenant
# ❌ BAD: Query without tenant_id
SELECT * FROM activities WHERE user_id = $1
(Missing tenant_id filter!)
# ❌ BAD: Tenant ID from request body instead of JWT
let tenant_id = params.tenant_id; // User can forge!
OAuth Credential Isolation
echo "🔐 Checking OAuth credential isolation..."
rg "oauth_token|refresh_token|access_token" src/database/ --type rust -A 3 | \
rg "tenant_id" | wc -l
echo "OAuth token queries with tenant_id scoping"
rg "provider.*credential|strava.*token|garmin.*token" src/ --type rust -A 5 | \
rg -v "tenant_id" | rg "SELECT|INSERT|UPDATE" && \
echo "⚠️ Provider credential query without tenant_id!" || \
echo "✓ Provider credentials properly tenant-scoped"
Config Write/Delete Tenant Scoping
echo "🔐 Checking config write/delete tenant scoping..."
rg "fn.*config.*(create|update|delete|write|remove)" src/ --type rust -A 10 | \
rg "tenant_id" | wc -l
echo "Config mutation functions with tenant_id check"
rg "fn.*(assign|remove|update).*coach|fn.*(assign|remove|update).*user" src/ --type rust -A 10 | \
rg "tenant_id" | wc -l
echo "Admin tool functions with tenant_id verification"
LLM API Key Isolation
echo "🔐 Checking LLM API key isolation..."
rg "llm.*key|ai.*key|gemini.*key|groq.*key|ollama.*url" src/ --type rust -A 5 | \
rg "tenant_id" | wc -l
echo "LLM key storage/retrieval with tenant_id scoping"
Success Criteria
- ✅ All multi-tenant tests pass
- ✅ Cross-tenant access attempts fail (403 or empty)
- ✅ All database queries include tenant_id filter
- ✅ TenantContext used in all route handlers
- ✅ No hardcoded tenant IDs in code
- ✅ OAuth tokens isolated per tenant
- ✅ API keys isolated per tenant
- ✅ Zero data leakage in logs (PII redaction active)
- ✅ Provider credentials (Strava, Garmin) tenant-scoped
- ✅ Config write/delete operations verify tenant membership
- ✅ LLM API keys stored and retrieved per-tenant
- ✅ Admin tools verify target belongs to caller's tenant
Related Files
tests/mcp_multitenant_complete_test.rs - Main test suite
src/tenant/mod.rs - TenantContext definition
src/middleware/tenant_middleware.rs - Tenant extraction
src/database/mod.rs - Scoped database queries
Related Skills
check-no-secrets - Secret detection
validate-architecture - Architectural validation