project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
This commit is contained in:
404
backend/tests/integration/test_tenant_isolation.py
Normal file
404
backend/tests/integration/test_tenant_isolation.py
Normal file
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
Integration test for multi-tenant data isolation.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
|
||||
|
||||
class TenantIsolationIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Super admin authentication header
|
||||
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer super_admin_token'}
|
||||
|
||||
# Tenant 1 authentication header
|
||||
self.tenant1_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant1_admin_token'}
|
||||
|
||||
# Tenant 2 authentication header
|
||||
self.tenant2_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant2_admin_token'}
|
||||
|
||||
# Test data for different tenants
|
||||
self.tenant1_user_data = {
|
||||
'email': 'user1@tenant1.com',
|
||||
'name': 'User One',
|
||||
'role': 'MANAGER',
|
||||
'department': 'Sales'
|
||||
}
|
||||
|
||||
self.tenant2_user_data = {
|
||||
'email': 'user1@tenant2.com',
|
||||
'name': 'User One Duplicate',
|
||||
'role': 'MANAGER',
|
||||
'department': 'Marketing'
|
||||
}
|
||||
|
||||
self.tenant1_product_data = {
|
||||
'sku': 'PROD-001',
|
||||
'name': 'Product A',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 999.99
|
||||
}
|
||||
|
||||
self.tenant2_product_data = {
|
||||
'sku': 'PROD-001', # Same SKU as tenant1
|
||||
'name': 'Product A Different',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 899.99
|
||||
}
|
||||
|
||||
def test_user_data_isolation(self):
|
||||
"""Test that user data is properly isolated between tenants."""
|
||||
# Step 1: Create users in different tenants with same email structure
|
||||
tenant1_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_user_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_user = tenant1_user_response.json()
|
||||
|
||||
tenant2_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_user_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_user = tenant2_user_response.json()
|
||||
|
||||
# Step 2: Verify tenant isolation - each tenant should only see their own users
|
||||
tenant1_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_users_response.status_code == status.HTTP_200_OK
|
||||
tenant1_users = tenant1_users_response.json()['users']
|
||||
|
||||
# Should only see users from tenant1
|
||||
assert len(tenant1_users) == 1
|
||||
assert tenant1_users[0]['email'] == self.tenant1_user_data['email']
|
||||
assert tenant1_users[0]['tenant_id'] == tenant1_user['tenant_id']
|
||||
|
||||
tenant2_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_users_response.status_code == status.HTTP_200_OK
|
||||
tenant2_users = tenant2_users_response.json()['users']
|
||||
|
||||
# Should only see users from tenant2
|
||||
assert len(tenant2_users) == 1
|
||||
assert tenant2_users[0]['email'] == self.tenant2_user_data['email']
|
||||
assert tenant2_users[0]['tenant_id'] == tenant2_user['tenant_id']
|
||||
|
||||
# Step 3: Super admin should see all users
|
||||
admin_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert admin_users_response.status_code == status.HTTP_200_OK
|
||||
admin_users = admin_users_response.json()['users']
|
||||
|
||||
# Should see users from both tenants
|
||||
assert len(admin_users) >= 2
|
||||
user_emails = [user['email'] for user in admin_users]
|
||||
assert self.tenant1_user_data['email'] in user_emails
|
||||
assert self.tenant2_user_data['email'] in user_emails
|
||||
|
||||
def test_product_data_isolation(self):
|
||||
"""Test that product data is properly isolated between tenants."""
|
||||
# Step 1: Create products with same SKU in different tenants
|
||||
tenant1_product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant1_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_product_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_product = tenant1_product_response.json()
|
||||
|
||||
tenant2_product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant2_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_product_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_product = tenant2_product_response.json()
|
||||
|
||||
# Step 2: Verify SKU isolation - same SKU allowed in different tenants
|
||||
assert tenant1_product['sku'] == tenant2_product['sku']
|
||||
assert tenant1_product['id'] != tenant2_product['id']
|
||||
assert tenant1_product['tenant_id'] != tenant2_product['tenant_id']
|
||||
|
||||
# Step 3: Test product retrieval isolation
|
||||
tenant1_products_response = self.client.get(
|
||||
'/api/v1/retail/products/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_products_response.status_code == status.HTTP_200_OK
|
||||
tenant1_products = tenant1_products_response.json()['products']
|
||||
|
||||
# Should only see products from tenant1
|
||||
assert len(tenant1_products) == 1
|
||||
assert tenant1_products[0]['name'] == self.tenant1_product_data['name']
|
||||
assert tenant1_products[0]['tenant_id'] == tenant1_product['tenant_id']
|
||||
|
||||
tenant2_products_response = self.client.get(
|
||||
'/api/v1/retail/products/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_products_response.status_code == status.HTTP_200_OK
|
||||
tenant2_products = tenant2_products_response.json()['products']
|
||||
|
||||
# Should only see products from tenant2
|
||||
assert len(tenant2_products) == 1
|
||||
assert tenant2_products[0]['name'] == self.tenant2_product_data['name']
|
||||
assert tenant2_products[0]['tenant_id'] == tenant2_product['tenant_id']
|
||||
|
||||
def test_healthcare_data_isolation(self):
|
||||
"""Test that healthcare patient data is properly isolated."""
|
||||
# Patient data for different tenants
|
||||
tenant1_patient_data = {
|
||||
'ic_number': '900101-10-1234',
|
||||
'name': 'Ahmad bin Hassan',
|
||||
'gender': 'MALE',
|
||||
'date_of_birth': '1990-01-01'
|
||||
}
|
||||
|
||||
tenant2_patient_data = {
|
||||
'ic_number': '900101-10-1234', # Same IC number
|
||||
'name': 'Ahmad bin Ali', # Different name
|
||||
'gender': 'MALE',
|
||||
'date_of_birth': '1990-01-01'
|
||||
}
|
||||
|
||||
# Create patients in different tenants
|
||||
tenant1_patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(tenant1_patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_patient_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_patient = tenant1_patient_response.json()
|
||||
|
||||
tenant2_patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(tenant2_patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_patient_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_patient = tenant2_patient_response.json()
|
||||
|
||||
# Verify same IC number allowed in different tenants (healthcare compliance)
|
||||
assert tenant1_patient['ic_number'] == tenant2_patient['ic_number']
|
||||
assert tenant1_patient['id'] != tenant2_patient['id']
|
||||
|
||||
# Test patient data isolation
|
||||
tenant1_patients_response = self.client.get(
|
||||
'/api/v1/healthcare/patients/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_patients_response.status_code == status.HTTP_200_OK
|
||||
tenant1_patients = tenant1_patients_response.json()['patients']
|
||||
|
||||
# Should only see patients from tenant1
|
||||
assert len(tenant1_patients) == 1
|
||||
assert tenant1_patients[0]['name'] == tenant1_patient_data['name']
|
||||
|
||||
def test_cross_tenant_access_prevention(self):
|
||||
"""Test that cross-tenant access is properly prevented."""
|
||||
# Step 1: Create a user in tenant1
|
||||
tenant1_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_user_response.status_code == status.HTTP_201_CREATED
|
||||
created_user = tenant1_user_response.json()
|
||||
user_id = created_user['id']
|
||||
|
||||
# Step 2: Try to access tenant1 user from tenant2 (should fail)
|
||||
tenant2_access_response = self.client.get(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_access_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# Step 3: Try to modify tenant1 user from tenant2 (should fail)
|
||||
modify_data = {'name': 'Hacked Name'}
|
||||
|
||||
tenant2_modify_response = self.client.put(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
data=json.dumps(modify_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_modify_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# Step 4: Verify user data is unchanged
|
||||
verify_response = self.client.get(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert verify_response.status_code == status.HTTP_200_OK
|
||||
verified_user = verify_response.json()
|
||||
assert verified_user['name'] == self.tenant1_user_data['name']
|
||||
|
||||
def test_database_row_level_security(self):
|
||||
"""Test that database row-level security is working."""
|
||||
# This test verifies that data isolation is enforced at the database level
|
||||
|
||||
# Create test data in both tenants
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
# Test direct database queries would be isolated
|
||||
# This is more of an integration test that would require actual database setup
|
||||
pass
|
||||
|
||||
def test_file_storage_isolation(self):
|
||||
"""Test that file storage is properly isolated between tenants."""
|
||||
# Upload files for different tenants
|
||||
# This would test file storage isolation mechanisms
|
||||
pass
|
||||
|
||||
def test_cache_isolation(self):
|
||||
"""Test that cache keys are properly isolated between tenants."""
|
||||
# Test that cache keys include tenant information
|
||||
# This ensures cache data doesn't leak between tenants
|
||||
pass
|
||||
|
||||
def test_tenant_context_propagation(self):
|
||||
"""Test that tenant context is properly propagated through the system."""
|
||||
# Create a user and verify tenant context is maintained across operations
|
||||
user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert user_response.status_code == status.HTTP_201_CREATED
|
||||
created_user = user_response.json()
|
||||
|
||||
# Verify tenant ID is consistently set
|
||||
assert 'tenant_id' in created_user
|
||||
tenant_id = created_user['tenant_id']
|
||||
|
||||
# Create a product and verify same tenant context
|
||||
product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant1_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert product_response.status_code == status.HTTP_201_CREATED
|
||||
created_product = product_response.json()
|
||||
|
||||
assert created_product['tenant_id'] == tenant_id
|
||||
|
||||
def test_tenant_configuration_isolation(self):
|
||||
"""Test that tenant configurations are properly isolated."""
|
||||
# Set tenant-specific configurations
|
||||
tenant1_config = {
|
||||
'timezone': 'Asia/Kuala_Lumpur',
|
||||
'currency': 'MYR',
|
||||
'date_format': 'DD/MM/YYYY'
|
||||
}
|
||||
|
||||
tenant2_config = {
|
||||
'timezone': 'Asia/Singapore',
|
||||
'currency': 'SGD',
|
||||
'date_format': 'MM/DD/YYYY'
|
||||
}
|
||||
|
||||
# Apply configurations (would need actual config endpoints)
|
||||
# Verify configurations don't interfere
|
||||
pass
|
||||
|
||||
def test_tenant_performance_isolation(self):
|
||||
"""Test that one tenant's performance doesn't affect others."""
|
||||
# This would test resource limits and performance isolation
|
||||
pass
|
||||
|
||||
def test_audit_log_tenant_isolation(self):
|
||||
"""Test that audit logs are properly isolated by tenant."""
|
||||
# Perform actions in different tenants
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
# Check that each tenant only sees their own audit logs
|
||||
tenant1_audit_response = self.client.get(
|
||||
'/api/v1/audit/logs/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_audit_response.status_code == status.HTTP_200_OK
|
||||
tenant1_logs = tenant1_audit_response.json()['logs']
|
||||
|
||||
# Should only see logs from tenant1 operations
|
||||
for log in tenant1_logs:
|
||||
assert log['tenant_id'] is not None
|
||||
|
||||
# Super admin should see all logs
|
||||
admin_audit_response = self.client.get(
|
||||
'/api/v1/audit/logs/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert admin_audit_response.status_code == status.HTTP_200_OK
|
||||
admin_logs = admin_audit_response.json()['logs']
|
||||
|
||||
# Should see logs from both tenants
|
||||
assert len(admin_logs) >= len(tenant1_logs)
|
||||
Reference in New Issue
Block a user