bottleneckcrm/backend/common/audit_log.py

307 lines
9.4 KiB
Python

"""
Security Audit Logging for Multi-Tenancy
Tracks security-relevant events for compliance and incident investigation:
- Login attempts (success/failure)
- Org switches
- Permission denials
- API key usage
- Suspicious activities
Usage:
from common.audit_log import audit_log
audit_log.login_success(user, org, request)
audit_log.org_switch(user, from_org, to_org, request)
audit_log.permission_denied(user, org, action, resource, request)
"""
import logging
from django.db import models
from django.utils import timezone
from common.base import BaseModel
logger = logging.getLogger("security.audit")
class SecurityAuditLog(BaseModel):
"""
Model to store security audit events.
This is separate from Activity model which tracks business operations.
SecurityAuditLog tracks authentication and authorization events.
"""
EVENT_TYPES = (
("LOGIN_SUCCESS", "Login Success"),
("LOGIN_FAILURE", "Login Failure"),
("LOGOUT", "Logout"),
("ORG_SWITCH", "Organization Switch"),
("TOKEN_REFRESH", "Token Refresh"),
("TOKEN_REVOKED", "Token Revoked"),
("PERMISSION_DENIED", "Permission Denied"),
("CROSS_ORG_ATTEMPT", "Cross-Org Access Attempt"),
("API_KEY_USED", "API Key Used"),
("API_KEY_INVALID", "Invalid API Key"),
("MEMBERSHIP_REVOKED", "Membership Revoked"),
("SUSPICIOUS_ACTIVITY", "Suspicious Activity"),
)
event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
user = models.ForeignKey(
"common.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="security_audit_logs",
)
org = models.ForeignKey(
"common.Org",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="security_audit_logs",
)
# Event details
description = models.TextField(blank=True, default="")
metadata = models.JSONField(default=dict, blank=True)
# Request information
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True, default="")
request_path = models.CharField(max_length=500, blank=True, default="")
request_method = models.CharField(max_length=10, blank=True, default="")
# Outcome
success = models.BooleanField(default=True)
class Meta:
verbose_name = "Security Audit Log"
verbose_name_plural = "Security Audit Logs"
db_table = "security_audit_log"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["event_type", "-created_at"]),
models.Index(fields=["user", "-created_at"]),
models.Index(fields=["org", "-created_at"]),
models.Index(fields=["ip_address"]),
models.Index(fields=["success", "-created_at"]),
]
def __str__(self):
return f"{self.event_type} - {self.user} - {self.created_at}"
class AuditLogger:
"""
Helper class for logging security events.
Logs to both database (SecurityAuditLog) and Python logger.
"""
def _get_request_info(self, request):
"""Extract request information for logging."""
if not request:
return {}
# Get IP address
ip = request.META.get("HTTP_X_FORWARDED_FOR")
if ip:
ip = ip.split(",")[0].strip()
else:
ip = request.META.get("REMOTE_ADDR")
return {
"ip_address": ip,
"user_agent": request.META.get("HTTP_USER_AGENT", "")[:500],
"request_path": request.path[:500],
"request_method": request.method,
}
def _log(
self,
event_type,
user=None,
org=None,
description="",
metadata=None,
success=True,
request=None,
):
"""
Internal method to create audit log entry.
"""
request_info = self._get_request_info(request)
# Log to database
try:
SecurityAuditLog.objects.create(
event_type=event_type,
user=user,
org=org,
description=description,
metadata=metadata or {},
success=success,
**request_info,
)
except Exception as e:
logger.error(f"Failed to create audit log: {e}")
# Log to Python logger
log_level = logging.INFO if success else logging.WARNING
user_id = str(user.id) if user else "anonymous"
org_id = str(org.id) if org else "none"
logger.log(
log_level,
f"{event_type} | user_id={user_id} | org_id={org_id} | "
f"ip={request_info.get('ip_address')} | success={success}",
)
def login_success(self, user, org, request=None):
"""Log successful login."""
self._log(
"LOGIN_SUCCESS",
user=user,
org=org,
description=f"User logged in to {org.name if org else 'platform'}",
request=request,
)
def login_failure(self, email, reason, request=None):
"""Log failed login attempt."""
from common.models import User
user = User.objects.filter(email=email).first()
self._log(
"LOGIN_FAILURE",
user=user,
description=f"Login failed for {email}: {reason}",
metadata={"email": email, "reason": reason},
success=False,
request=request,
)
def logout(self, user, org, request=None):
"""Log user logout."""
self._log(
"LOGOUT",
user=user,
org=org,
description=f"User logged out",
request=request,
)
def org_switch(self, user, from_org, to_org, request=None):
"""Log organization switch."""
self._log(
"ORG_SWITCH",
user=user,
org=to_org,
description=f"Switched from {from_org.name if from_org else 'none'} to {to_org.name}",
metadata={
"from_org_id": str(from_org.id) if from_org else None,
"to_org_id": str(to_org.id),
},
request=request,
)
def token_refresh(self, user, org, request=None):
"""Log token refresh."""
self._log(
"TOKEN_REFRESH",
user=user,
org=org,
description="Token refreshed",
request=request,
)
def token_revoked(self, user, org, reason, request=None):
"""Log token revocation."""
self._log(
"TOKEN_REVOKED",
user=user,
org=org,
description=f"Token revoked: {reason}",
metadata={"reason": reason},
request=request,
)
def permission_denied(self, user, org, action, resource, request=None):
"""Log permission denial."""
self._log(
"PERMISSION_DENIED",
user=user,
org=org,
description=f"Permission denied for {action} on {resource}",
metadata={"action": action, "resource": resource},
success=False,
request=request,
)
def cross_org_attempt(self, user, user_org, target_org, resource, request=None):
"""Log attempted cross-org data access."""
self._log(
"CROSS_ORG_ATTEMPT",
user=user,
org=user_org,
description=f"Attempted to access {resource} in org {target_org.name if target_org else 'unknown'}",
metadata={
"user_org_id": str(user_org.id) if user_org else None,
"target_org_id": str(target_org.id) if target_org else None,
"resource": resource,
},
success=False,
request=request,
)
def api_key_used(self, org, endpoint, request=None):
"""Log API key usage."""
self._log(
"API_KEY_USED",
org=org,
description=f"API key used for {endpoint}",
metadata={"endpoint": endpoint},
request=request,
)
def api_key_invalid(self, api_key_prefix, request=None):
"""Log invalid API key attempt."""
self._log(
"API_KEY_INVALID",
description=f"Invalid API key attempted: {api_key_prefix}...",
metadata={"api_key_prefix": api_key_prefix},
success=False,
request=request,
)
def membership_revoked(self, user, org, revoked_by=None, request=None):
"""Log when user's org membership is revoked."""
self._log(
"MEMBERSHIP_REVOKED",
user=user,
org=org,
description=f"Membership revoked by {revoked_by.email if revoked_by else 'system'}",
metadata={"revoked_by": str(revoked_by.id) if revoked_by else None},
request=request,
)
def suspicious_activity(self, user, org, activity_type, details, request=None):
"""Log suspicious activity for security review."""
self._log(
"SUSPICIOUS_ACTIVITY",
user=user,
org=org,
description=f"Suspicious activity: {activity_type}",
metadata={"activity_type": activity_type, "details": details},
success=False,
request=request,
)
# Global audit logger instance
audit_log = AuditLogger()