Based on the git diff provided, here's a concise and descriptive commit message:

feat: add security event taxonomy and optimize park queryset

- Add comprehensive security_event_types ChoiceGroup with categories for authentication, MFA, password, account, session, and API key events
- Include severity levels, icons, and CSS classes for each event type
- Fix park queryset optimization by using select_related for OneToOne location relationship
- Remove location property fields (latitude/longitude) from values() call as they are not actual DB columns
- Add proper location fields (city, state, country) to values() for map display

This change enhances security event tracking capabilities and resolves a queryset optimization issue where property decorators were incorrectly used in values() queries.
This commit is contained in:
pacnpal
2026-01-10 16:41:31 -05:00
parent 96df23242e
commit 2b66814d82
26 changed files with 2055 additions and 112 deletions

View File

@@ -620,6 +620,111 @@ class NotificationPreference(TrackedModel):
return getattr(self, field_name, False)
@pghistory.track()
class SecurityLog(models.Model):
"""
Model to track security-relevant authentication events.
All security-critical events are logged here for audit purposes,
including logins, MFA changes, password changes, and session management.
"""
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="security_logs",
null=True, # Allow null for failed login attempts with no valid user
blank=True,
help_text="User this event is associated with",
)
event_type = RichChoiceField(
choice_group="security_event_types",
domain="accounts",
max_length=50,
db_index=True,
help_text="Type of security event",
)
ip_address = models.GenericIPAddressField(
help_text="IP address of the request",
)
user_agent = models.TextField(
blank=True,
help_text="User agent string from the request",
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional event-specific data",
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text="When this event occurred",
)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "-created_at"]),
models.Index(fields=["event_type", "-created_at"]),
models.Index(fields=["ip_address", "-created_at"]),
]
verbose_name = "Security Log"
verbose_name_plural = "Security Logs"
def __str__(self):
username = self.user.username if self.user else "Unknown"
return f"{self.get_event_type_display()} - {username} at {self.created_at}"
@classmethod
def log_event(
cls,
event_type: str,
ip_address: str,
user=None,
user_agent: str = "",
metadata: dict = None,
) -> "SecurityLog":
"""
Create a new security log entry.
Args:
event_type: One of security_event_types choices (e.g., "login_success")
ip_address: Client IP address
user: User instance (optional for failed logins)
user_agent: Browser user agent string
metadata: Additional event-specific data
Returns:
The created SecurityLog instance
"""
return cls.objects.create(
user=user,
event_type=event_type,
ip_address=ip_address,
user_agent=user_agent,
metadata=metadata or {},
)
@classmethod
def get_recent_for_user(cls, user, limit: int = 20):
"""Get recent security events for a user."""
return cls.objects.filter(user=user).order_by("-created_at")[:limit]
@classmethod
def get_failed_login_count(cls, ip_address: str, minutes: int = 15) -> int:
"""Count failed login attempts from an IP in the last N minutes."""
from datetime import timedelta
from django.utils import timezone
cutoff = timezone.now() - timedelta(minutes=minutes)
return cls.objects.filter(
event_type="login_failed",
ip_address=ip_address,
created_at__gte=cutoff,
).count()
# Signal handlers for automatic notification preference creation