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

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,806 @@
"""
Security middleware for comprehensive protection.
Implements Malaysian data protection and security best practices.
"""
import re
import json
import logging
import time
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from django.conf import settings
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.core.cache import cache
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.middleware.security import SecurityMiddleware as DjangoSecurityMiddleware
from django.middleware.clickjacking import XFrameOptionsMiddleware
from django.middleware.csrf import CsrfViewMiddleware
from django.views.decorators.csrf import csrf_exempt
from django.utils.deprecation import MiddlewareMixin
from prometheus_client import Counter, Histogram, Gauge
import redis
logger = logging.getLogger(__name__)
User = get_user_model()
# Security metrics
SECURITY_EVENTS = Counter(
'security_events_total',
'Security events',
['event_type', 'severity', 'ip_address', 'user_agent', 'tenant']
)
RATE_LIMIT_EVENTS = Counter(
'rate_limit_events_total',
'Rate limit events',
['type', 'ip_address', 'endpoint', 'tenant']
)
MALAYSIAN_DATA_ACCESS = Counter(
'malaysian_data_access_total',
'Malaysian data access events',
['data_type', 'operation', 'user_role', 'tenant']
)
THREAT_DETECTION = Counter(
'threat_detection_total',
'Threat detection events',
['threat_type', 'confidence', 'ip_address', 'tenant']
)
class SecurityHeadersMiddleware(MiddlewareMixin):
"""Enhanced security headers middleware."""
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
"""Add comprehensive security headers."""
# Security headers
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response['Permissions-Policy'] = self._get_permissions_policy()
response['Content-Security-Policy'] = self._get_csp(request)
response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
# Malaysian data protection headers
response['X-Malaysian-Data-Protection'] = 'PDPA-Compliant'
response['X-Data-Residency'] = 'Malaysia'
# Remove sensitive headers
sensitive_headers = ['Server', 'X-Powered-By', 'X-AspNet-Version']
for header in sensitive_headers:
if header in response:
del response[header]
return response
def _get_permissions_policy(self) -> str:
"""Get permissions policy."""
policies = [
'accelerometer=()',
'ambient-light-sensor=()',
'battery=()',
'bluetooth=()',
'camera=()',
'cross-origin-isolated=()',
'display-capture=()',
'document-domain=()',
'encrypted-media=()',
'execution-while-not-rendered=()',
'execution-while-out-of-viewport=()',
'focus-without-user-activation=()',
'fullscreen=()',
'geolocation=()',
'gyroscope=()',
'hid=()',
'identity-credentials-get=()',
'idle-detection=()',
'local-fonts=()',
'magnetometer=()',
'microphone=()',
'midi=()',
'otp-credentials=()',
'payment=()',
'picture-in-picture=()',
'publickey-credentials-get=()',
'screen-wake-lock=()',
'serial=()',
'storage-access=()',
'usb=()',
'web-share=()',
'window-management=()',
'xr-spatial-tracking=()'
]
return ', '.join(policies)
def _get_csp(self, request: HttpRequest) -> str:
"""Get Content Security Policy."""
# Base CSP
csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://www.google.com https://www.gstatic.com",
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com",
"img-src 'self' data: https: https://*.malaysian-sme-platform.com",
"font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com",
"connect-src 'self' https://api.malaysian-sme-platform.com wss://api.malaysian-sme-platform.com",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
"require-trusted-types-for 'script'",
"report-uri /api/security/csp-report/",
]
# Add development-specific policies
if settings.DEBUG:
csp[1] = csp[1].replace("'unsafe-inline'", "'unsafe-inline' 'unsafe-eval'")
csp.append("upgrade-insecure-requests")
return '; '.join(csp)
class RateLimitingMiddleware(MiddlewareMixin):
"""Advanced rate limiting middleware with Malaysian considerations."""
def __init__(self, get_response):
self.get_response = get_response
self.redis_client = self._get_redis_client()
self.limits = self._get_rate_limits()
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
"""Process request for rate limiting."""
if self._should_skip_rate_limiting(request):
return None
ip_address = self._get_client_ip(request)
user_id = self._get_user_id(request)
endpoint = self._get_endpoint(request)
tenant = self._get_tenant_info(request)
# Check all applicable limits
for limit_type, limit_config in self.limits.items():
if self._should_apply_limit(request, limit_type):
limited = self._check_rate_limit(
ip_address, user_id, endpoint, tenant, limit_type, limit_config
)
if limited:
return self._create_rate_limit_response(limit_type, limit_config)
return None
def _get_redis_client(self):
"""Get Redis client for rate limiting."""
try:
return redis.from_url(settings.REDIS_URL)
except Exception:
logger.warning("Redis not available for rate limiting")
return None
def _get_rate_limits(self) -> Dict[str, Dict[str, Any]]:
"""Get rate limit configurations."""
return {
'api': {
'requests': 1000,
'window': 3600, # 1 hour
'scope': 'ip',
'message': 'API rate limit exceeded'
},
'login': {
'requests': 5,
'window': 300, # 5 minutes
'scope': 'ip',
'message': 'Too many login attempts'
},
'malaysian_data': {
'requests': 100,
'window': 3600, # 1 hour
'scope': 'user',
'message': 'Malaysian data access rate limit exceeded'
},
'file_upload': {
'requests': 50,
'window': 3600, # 1 hour
'scope': 'user',
'message': 'File upload rate limit exceeded'
},
'sensitive_operations': {
'requests': 10,
'window': 3600, # 1 hour
'scope': 'user',
'message': 'Sensitive operations rate limit exceeded'
},
}
def _should_skip_rate_limiting(self, request: HttpRequest) -> bool:
"""Check if request should skip rate limiting."""
# Skip for health checks and static files
skip_paths = ['/health/', '/metrics/', '/static/']
if any(request.path.startswith(path) for path in skip_paths):
return True
# Skip for authenticated staff users
if hasattr(request, 'user') and request.user.is_authenticated and request.user.is_staff:
return True
return False
def _get_client_ip(self, request: HttpRequest) -> str:
"""Get client IP address with proxy support."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR', 'unknown')
# Handle IPv6 loopback
if ip == '::1':
ip = '127.0.0.1'
return ip
def _get_user_id(self, request: HttpRequest) -> Optional[str]:
"""Get user ID for rate limiting."""
if hasattr(request, 'user') and request.user.is_authenticated:
return str(request.user.id)
return None
def _get_endpoint(self, request: HttpRequest) -> str:
"""Get endpoint for rate limiting."""
return request.path
def _get_tenant_info(self, request: HttpRequest) -> Dict[str, Any]:
"""Get tenant information."""
if hasattr(request, 'tenant') and request.tenant:
return {
'id': request.tenant.id,
'name': request.tenant.name,
'schema': request.tenant.schema_name
}
return {'id': None, 'name': 'public', 'schema': 'public'}
def _should_apply_limit(self, request: HttpRequest, limit_type: str) -> bool:
"""Check if limit should be applied to request."""
if limit_type == 'api' and request.path.startswith('/api/'):
return True
elif limit_type == 'login' and '/login' in request.path:
return True
elif limit_type == 'malaysian_data' and self._is_malaysian_data_endpoint(request):
return True
elif limit_type == 'file_upload' and request.method == 'POST' and 'upload' in request.path:
return True
elif limit_type == 'sensitive_operations' and self._is_sensitive_operation(request):
return True
return False
def _is_malaysian_data_endpoint(self, request: HttpRequest) -> bool:
"""Check if endpoint accesses Malaysian data."""
malaysian_endpoints = [
'/api/malaysian/',
'/api/ic-validation/',
'/api/sst/',
'/api/postcode/',
'/api/business-registration/',
]
return any(request.path.startswith(endpoint) for endpoint in malaysian_endpoints)
def _is_sensitive_operation(self, request: HttpRequest) -> bool:
"""Check if operation is sensitive."""
sensitive_operations = [
'/api/users/',
'/api/tenants/',
'/api/admin/',
'/api/payments/',
'/api/export/',
]
return any(request.path.startswith(op) for op in sensitive_operations)
def _check_rate_limit(
self,
ip_address: str,
user_id: Optional[str],
endpoint: str,
tenant: Dict[str, Any],
limit_type: str,
limit_config: Dict[str, Any]
) -> bool:
"""Check if rate limit is exceeded."""
if not self.redis_client:
return False
# Generate key based on scope
if limit_config['scope'] == 'user' and user_id:
key = f"rate_limit:{limit_type}:{user_id}:{tenant['id']}"
else:
key = f"rate_limit:{limit_type}:{ip_address}:{tenant['id']}"
# Check current count
current_count = self.redis_client.get(key)
if current_count is None:
current_count = 0
else:
current_count = int(current_count)
# Check if limit exceeded
if current_count >= limit_config['requests']:
RATE_LIMIT_EVENTS.labels(
type=limit_type,
ip_address=ip_address,
endpoint=endpoint,
tenant=tenant.get('name', 'unknown')
).inc()
SECURITY_EVENTS.labels(
event_type='rate_limit_exceeded',
severity='warning',
ip_address=ip_address,
user_agent=request.META.get('HTTP_USER_AGENT', 'unknown'),
tenant=tenant.get('name', 'unknown')
).inc()
return True
# Increment counter
self.redis_client.incr(key)
self.redis_client.expire(key, limit_config['window'])
return False
def _create_rate_limit_response(self, limit_type: str, limit_config: Dict[str, Any]) -> JsonResponse:
"""Create rate limit response."""
response_data = {
'error': limit_config['message'],
'type': 'rate_limit_exceeded',
'retry_after': limit_config['window'],
'limit_type': limit_type
}
return JsonResponse(response_data, status=429)
class InputValidationMiddleware(MiddlewareMixin):
"""Input validation and sanitization middleware."""
def __init__(self, get_response):
self.get_response = get_response
self.suspicious_patterns = self._get_suspicious_patterns()
self.max_input_size = getattr(settings, 'MAX_INPUT_SIZE', 1024 * 1024) # 1MB
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
"""Validate and sanitize input."""
# Check input size
if not self._check_input_size(request):
return JsonResponse(
{'error': 'Request size too large'},
status=413
)
# Validate input for POST/PUT/PATCH
if request.method in ['POST', 'PUT', 'PATCH']:
if not self._validate_input(request):
return JsonResponse(
{'error': 'Invalid input detected'},
status=400
)
return None
def _get_suspicious_patterns(self) -> List[re.Pattern]:
"""Get suspicious input patterns."""
patterns = [
# SQL injection
re.compile(r'(?i)\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|EXEC|ALTER|CREATE|TRUNCATE)\b.*\b(FROM|INTO|TABLE|DATABASE)\b'),
re.compile(r'(?i)\b(OR\s+1\s*=\s*1|OR\s+TRUE|AND\s+1\s*=\s*1)\b'),
re.compile(r'(?i)\b(WAITFOR\s+DELAY|SLEEP\(|PG_SLEEP\(|BENCHMARK\()\b'),
# XSS
re.compile(r'(?i)<(script|iframe|object|embed|form|input)\b.*?>'),
re.compile(r'(?i)javascript:'),
re.compile(r'(?i)on\w+\s*='),
re.compile(r'(?i)(eval|Function|setTimeout|setInterval)\s*\('),
# Path traversal
re.compile(r'\.\./'),
re.compile(r'(?i)\b(/etc|/var|/usr|/home)/'),
re.compile(r'(?i)\.(htaccess|htpasswd|env)\b'),
# Command injection
re.compile(r'(?i);\s*(rm|ls|cat|pwd|whoami|id|ps|netstat|curl|wget)\s'),
re.compile(r'(?i)\|(\s*)(rm|ls|cat|pwd|whoami|id|ps|netstat|curl|wget)\s'),
re.compile(r'(?i)&(\s*)(rm|ls|cat|pwd|whoami|id|ps|netstat|curl|wget)\s'),
# NoSQL injection
re.compile(r'(?i)\$where\b'),
re.compile(r'(?i)\b(db\.eval|mapReduce|group)\b'),
# LDAP injection
re.compile(r'(?i)\*\)\)(\|\()'),
re.compile(r'(?i)\)\)(\|\()'),
# XML injection
re.compile(r'<!ENTITY\s+'),
re.compile(r'<\?xml\s+'),
]
return patterns
def _check_input_size(self, request: HttpRequest) -> bool:
"""Check if request size is within limits."""
# Check GET parameters
for key, value in request.GET.items():
if len(str(value)) > self.max_input_size:
return False
# Check POST data
if hasattr(request, 'POST'):
for key, value in request.POST.items():
if len(str(value)) > self.max_input_size:
return False
# Check JSON body
if hasattr(request, 'body') and request.body:
if len(request.body) > self.max_input_size:
return False
return True
def _validate_input(self, request: HttpRequest) -> bool:
"""Validate request input for malicious patterns."""
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
# Check GET parameters
for key, value in request.GET.items():
if self._contains_suspicious_pattern(str(value)):
SECURITY_EVENTS.labels(
event_type='suspicious_input',
severity='warning',
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
return False
# Check POST data
if hasattr(request, 'POST'):
for key, value in request.POST.items():
if self._contains_suspicious_pattern(str(value)):
SECURITY_EVENTS.labels(
event_type='suspicious_input',
severity='warning',
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
return False
# Check JSON body
if hasattr(request, 'body') and request.body:
try:
body_str = request.body.decode('utf-8')
if self._contains_suspicious_pattern(body_str):
SECURITY_EVENTS.labels(
event_type='suspicious_input',
severity='warning',
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
return False
except UnicodeDecodeError:
SECURITY_EVENTS.labels(
event_type='invalid_encoding',
severity='warning',
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
return False
return True
def _contains_suspicious_pattern(self, input_str: str) -> bool:
"""Check if input contains suspicious patterns."""
for pattern in self.suspicious_patterns:
if pattern.search(input_str):
return True
return False
def _get_client_ip(self, request: HttpRequest) -> str:
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', 'unknown')
def _get_tenant_info(self, request: HttpRequest) -> Dict[str, Any]:
"""Get tenant information."""
if hasattr(request, 'tenant') and request.tenant:
return {
'id': request.tenant.id,
'name': request.tenant.name,
'schema': request.tenant.schema_name
}
return {'id': None, 'name': 'public', 'schema': 'public'}
class DataProtectionMiddleware(MiddlewareMixin):
"""Malaysian data protection compliance middleware."""
def __init__(self, get_response):
self.get_response = get_response
self.sensitive_data_fields = self._get_sensitive_data_fields()
self.required_consent_version = getattr(settings, 'REQUIRED_CONSENT_VERSION', '1.0')
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
"""Process response for data protection."""
# Add Malaysian data protection headers
response['X-Malaysian-Data-Protection'] = 'PDPA-Compliant'
response['X-Data-Residency'] = 'Malaysia'
# Log Malaysian data access
if self._is_malaysian_data_access(request):
self._log_malaysian_data_access(request)
# Sanitize response data
if hasattr(response, 'data') and isinstance(response.data, dict):
response.data = self._sanitize_response_data(response.data)
return response
def _get_sensitive_data_fields(self) -> List[str]:
"""Get sensitive data fields that require protection."""
return [
'ic_number',
'passport_number',
'email',
'phone_number',
'address',
'bank_account',
'salary',
'business_registration_number',
'tax_id',
]
def _is_malaysian_data_access(self, request: HttpRequest) -> bool:
"""Check if request accesses Malaysian data."""
malaysian_endpoints = [
'/api/malaysian/',
'/api/ic-validation/',
'/api/sst/',
'/api/postcode/',
'/api/business-registration/',
]
return any(request.path.startswith(endpoint) for endpoint in malaysian_endpoints)
def _log_malaysian_data_access(self, request: HttpRequest):
"""Log Malaysian data access for compliance."""
user_role = 'anonymous'
if hasattr(request, 'user') and request.user.is_authenticated:
user_role = request.user.role
tenant = self._get_tenant_info(request)
# Determine data type
data_type = 'unknown'
if '/ic-validation/' in request.path:
data_type = 'ic_data'
elif '/sst/' in request.path:
data_type = 'tax_data'
elif '/postcode/' in request.path:
data_type = 'location_data'
elif '/business-registration/' in request.path:
data_type = 'business_data'
MALAYSIAN_DATA_ACCESS.labels(
data_type=data_type,
operation=request.method,
user_role=user_role,
tenant=tenant.get('name', 'unknown')
).inc()
def _sanitize_response_data(self, data: Any) -> Any:
"""Sanitize response data to remove sensitive information."""
if isinstance(data, dict):
sanitized = {}
for key, value in data.items():
if key.lower() in [field.lower() for field in self.sensitive_data_fields]:
sanitized[key] = self._mask_sensitive_data(key, value)
else:
sanitized[key] = self._sanitize_response_data(value)
return sanitized
elif isinstance(data, list):
return [self._sanitize_response_data(item) for item in data]
else:
return data
def _mask_sensitive_data(self, field: str, value: Any) -> str:
"""Mask sensitive data for logging/display."""
if field.lower() in ['ic_number', 'passport_number']:
return value[:2] + '*' * (len(value) - 4) + value[-2:]
elif field.lower() in ['email']:
return value[:3] + '*' * (len(value.split('@')[0]) - 3) + '@' + value.split('@')[1]
elif field.lower() in ['phone_number']:
return value[:3] + '*' * (len(value) - 6) + value[-3:]
elif field.lower() in ['bank_account']:
return '*' * (len(value) - 4) + value[-4:]
else:
return '*' * len(str(value))
def _get_tenant_info(self, request: HttpRequest) -> Dict[str, Any]:
"""Get tenant information."""
if hasattr(request, 'tenant') and request.tenant:
return {
'id': request.tenant.id,
'name': request.tenant.name,
'schema': request.tenant.schema_name
}
return {'id': None, 'name': 'public', 'schema': 'public'}
class SecurityLoggingMiddleware(MiddlewareMixin):
"""Security event logging middleware."""
def __init__(self, get_response):
self.get_response = get_response
self.security_log_fields = [
'ip_address',
'user_agent',
'timestamp',
'endpoint',
'method',
'user_id',
'tenant',
'event_type',
'severity',
'details'
]
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
"""Log security-relevant requests."""
# Log authentication attempts
if '/login' in request.path or '/auth/' in request.path:
self._log_auth_attempt(request)
# Log admin access
if '/admin/' in request.path:
self._log_admin_access(request)
# Log Malaysian data access
if self._is_malaysian_data_access(request):
self._log_malaysian_access(request)
return None
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
"""Log security-relevant responses."""
# Log failed requests
if response.status_code >= 400:
self._log_failed_request(request, response)
# Log rate limiting
if response.status_code == 429:
self._log_rate_limit(request, response)
return response
def _log_auth_attempt(self, request: HttpRequest):
"""Log authentication attempt."""
event_type = 'login_attempt'
severity = 'info'
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
SECURITY_EVENTS.labels(
event_type=event_type,
severity=severity,
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
def _log_admin_access(self, request: HttpRequest):
"""Log admin area access."""
if not hasattr(request, 'user') or not request.user.is_authenticated:
event_type = 'unauthorized_admin_access'
severity = 'warning'
elif not request.user.is_staff:
event_type = 'unauthorized_admin_access'
severity = 'warning'
else:
event_type = 'admin_access'
severity = 'info'
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
SECURITY_EVENTS.labels(
event_type=event_type,
severity=severity,
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
def _log_malaysian_access(self, request: HttpRequest):
"""Log Malaysian data access."""
event_type = 'malaysian_data_access'
severity = 'info'
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
SECURITY_EVENTS.labels(
event_type=event_type,
severity=severity,
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
def _log_failed_request(self, request: HttpRequest, response: HttpResponse):
"""Log failed requests."""
event_type = 'failed_request'
severity = 'warning' if response.status_code < 500 else 'error'
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
SECURITY_EVENTS.labels(
event_type=event_type,
severity=severity,
ip_address=ip_address,
user_agent=user_agent,
tenant=tenant.get('name', 'unknown')
).inc()
def _log_rate_limit(self, request: HttpRequest, response: HttpResponse):
"""Log rate limiting events."""
event_type = 'rate_limit'
severity = 'warning'
ip_address = self._get_client_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
tenant = self._get_tenant_info(request)
RATE_LIMIT_EVENTS.labels(
type='api',
ip_address=ip_address,
endpoint=request.path,
tenant=tenant.get('name', 'unknown')
).inc()
def _get_client_ip(self, request: HttpRequest) -> str:
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', 'unknown')
def _get_tenant_info(self, request: HttpRequest) -> Dict[str, Any]:
"""Get tenant information."""
if hasattr(request, 'tenant') and request.tenant:
return {
'id': request.tenant.id,
'name': request.tenant.name,
'schema': request.tenant.schema_name
}
return {'id': None, 'name': 'public', 'schema': 'public'}
def _is_malaysian_data_access(self, request: HttpRequest) -> bool:
"""Check if request accesses Malaysian data."""
malaysian_endpoints = [
'/api/malaysian/',
'/api/ic-validation/',
'/api/sst/',
'/api/postcode/',
'/api/business-registration/',
]
return any(request.path.startswith(endpoint) for endpoint in malaysian_endpoints)