mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 11:25:19 -05:00
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:
@@ -64,6 +64,23 @@ def get_mfa_status(request):
|
||||
except Authenticator.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Check for Discord social account with MFA enabled
|
||||
discord_mfa_enabled = False
|
||||
connected_provider = None
|
||||
|
||||
try:
|
||||
social_accounts = user.socialaccount_set.all()
|
||||
for social_account in social_accounts:
|
||||
if social_account.provider == "discord":
|
||||
connected_provider = "discord"
|
||||
discord_mfa_enabled = social_account.extra_data.get("mfa_enabled", False)
|
||||
break
|
||||
elif social_account.provider == "google":
|
||||
connected_provider = "google"
|
||||
# Google doesn't expose MFA status
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# has_second_factor is True if user has either TOTP or Passkey configured
|
||||
has_second_factor = totp_enabled or passkey_enabled
|
||||
|
||||
@@ -76,6 +93,9 @@ def get_mfa_status(request):
|
||||
"recovery_codes_enabled": recovery_enabled,
|
||||
"recovery_codes_count": recovery_count,
|
||||
"has_second_factor": has_second_factor,
|
||||
# New fields for enhanced MFA satisfaction
|
||||
"discord_mfa_enabled": discord_mfa_enabled,
|
||||
"connected_provider": connected_provider,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -100,6 +120,8 @@ def get_mfa_status(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def setup_totp(request):
|
||||
"""Generate TOTP secret and QR code for setup."""
|
||||
from django.utils import timezone
|
||||
|
||||
from allauth.mfa.totp.internal import auth as totp_auth
|
||||
|
||||
user = request.user
|
||||
@@ -120,14 +142,16 @@ def setup_totp(request):
|
||||
qr.save(buffer, format="PNG")
|
||||
qr_code_base64 = f"data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}"
|
||||
|
||||
# Store secret in session for later verification
|
||||
# Store secret in session for later verification with 15-minute expiry
|
||||
request.session["pending_totp_secret"] = secret
|
||||
request.session["pending_totp_expires"] = (timezone.now().timestamp() + 900) # 15 minutes
|
||||
|
||||
return Response(
|
||||
{
|
||||
"secret": secret,
|
||||
"provisioning_uri": uri,
|
||||
"qr_code_base64": qr_code_base64,
|
||||
"expires_in_seconds": 900,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -165,10 +189,17 @@ def setup_totp(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def activate_totp(request):
|
||||
"""Verify TOTP code and activate MFA."""
|
||||
from django.utils import timezone
|
||||
|
||||
from allauth.mfa.models import Authenticator
|
||||
from allauth.mfa.recovery_codes.internal.auth import RecoveryCodes
|
||||
from allauth.mfa.totp.internal import auth as totp_auth
|
||||
|
||||
from apps.accounts.services.security_service import (
|
||||
log_security_event,
|
||||
send_security_notification,
|
||||
)
|
||||
|
||||
user = request.user
|
||||
code = request.data.get("code", "").strip()
|
||||
|
||||
@@ -187,6 +218,19 @@ def activate_totp(request):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if setup has expired (15 minute timeout)
|
||||
expires_at = request.session.get("pending_totp_expires")
|
||||
if expires_at and timezone.now().timestamp() > expires_at:
|
||||
# Clear expired session data
|
||||
if "pending_totp_secret" in request.session:
|
||||
del request.session["pending_totp_secret"]
|
||||
if "pending_totp_expires" in request.session:
|
||||
del request.session["pending_totp_expires"]
|
||||
return Response(
|
||||
{"detail": "TOTP setup session expired. Please start setup again."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Verify the code
|
||||
if not totp_auth.validate_totp_code(secret, code):
|
||||
return Response(
|
||||
@@ -215,11 +259,25 @@ def activate_totp(request):
|
||||
# Clear session (only if it exists - won't exist with JWT auth + secret from body)
|
||||
if "pending_totp_secret" in request.session:
|
||||
del request.session["pending_totp_secret"]
|
||||
if "pending_totp_expires" in request.session:
|
||||
del request.session["pending_totp_expires"]
|
||||
|
||||
# Log security event
|
||||
log_security_event(
|
||||
"mfa_enrolled",
|
||||
request,
|
||||
user=user,
|
||||
metadata={"method": "totp"},
|
||||
)
|
||||
|
||||
# Send security notification email
|
||||
send_security_notification(user, "mfa_enrolled", {"method": "TOTP Authenticator"})
|
||||
|
||||
return Response(
|
||||
{
|
||||
"detail": "Two-factor authentication enabled",
|
||||
"recovery_codes": codes,
|
||||
"recovery_codes_count": len(codes),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -255,13 +313,59 @@ def deactivate_totp(request):
|
||||
"""Disable TOTP authentication."""
|
||||
from allauth.mfa.models import Authenticator
|
||||
|
||||
from apps.accounts.services.security_service import (
|
||||
check_auth_method_availability,
|
||||
log_security_event,
|
||||
send_security_notification,
|
||||
)
|
||||
|
||||
user = request.user
|
||||
password = request.data.get("password", "")
|
||||
recovery_code = request.data.get("recovery_code", "")
|
||||
|
||||
# Verify password
|
||||
if not user.check_password(password):
|
||||
# Check if user has other auth methods before we allow disabling MFA
|
||||
auth_methods = check_auth_method_availability(user)
|
||||
|
||||
# If TOTP is their only way in alongside passkeys, we need to ensure they have
|
||||
# at least password or social login to fall back on
|
||||
if not auth_methods["has_password"] and not auth_methods["has_social"] and not auth_methods["has_passkey"]:
|
||||
return Response(
|
||||
{"detail": "Invalid password"},
|
||||
{"detail": "Cannot disable MFA: you must have at least one authentication method. Please set a password or connect a social account first."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Verify password OR recovery code
|
||||
verified = False
|
||||
verification_method = None
|
||||
|
||||
if password and user.check_password(password):
|
||||
verified = True
|
||||
verification_method = "password"
|
||||
elif recovery_code:
|
||||
# Try to verify with recovery code
|
||||
try:
|
||||
recovery_auth = Authenticator.objects.get(
|
||||
user=user, type=Authenticator.Type.RECOVERY_CODES
|
||||
)
|
||||
unused_codes = recovery_auth.data.get("codes", [])
|
||||
if recovery_code.upper().replace("-", "").replace(" ", "") in [
|
||||
c.upper().replace("-", "").replace(" ", "") for c in unused_codes
|
||||
]:
|
||||
verified = True
|
||||
verification_method = "recovery_code"
|
||||
# Remove the used code
|
||||
unused_codes = [
|
||||
c for c in unused_codes
|
||||
if c.upper().replace("-", "").replace(" ", "") != recovery_code.upper().replace("-", "").replace(" ", "")
|
||||
]
|
||||
recovery_auth.data["codes"] = unused_codes
|
||||
recovery_auth.save()
|
||||
except Authenticator.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not verified:
|
||||
return Response(
|
||||
{"detail": "Invalid password or recovery code"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -276,6 +380,17 @@ def deactivate_totp(request):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Log security event
|
||||
log_security_event(
|
||||
"mfa_disabled",
|
||||
request,
|
||||
user=user,
|
||||
metadata={"method": "totp", "verified_via": verification_method},
|
||||
)
|
||||
|
||||
# Send security notification email
|
||||
send_security_notification(user, "mfa_disabled", {"method": "TOTP Authenticator"})
|
||||
|
||||
return Response(
|
||||
{
|
||||
"detail": "Two-factor authentication disabled",
|
||||
@@ -361,6 +476,11 @@ def regenerate_recovery_codes(request):
|
||||
from allauth.mfa.models import Authenticator
|
||||
from allauth.mfa.recovery_codes.internal.auth import RecoveryCodes
|
||||
|
||||
from apps.accounts.services.security_service import (
|
||||
log_security_event,
|
||||
send_security_notification,
|
||||
)
|
||||
|
||||
user = request.user
|
||||
password = request.data.get("password", "")
|
||||
|
||||
@@ -371,8 +491,11 @@ def regenerate_recovery_codes(request):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if TOTP is enabled
|
||||
if not Authenticator.objects.filter(user=user, type=Authenticator.Type.TOTP).exists():
|
||||
# Check if MFA is enabled (TOTP or Passkey)
|
||||
has_totp = Authenticator.objects.filter(user=user, type=Authenticator.Type.TOTP).exists()
|
||||
has_passkey = Authenticator.objects.filter(user=user, type=Authenticator.Type.WEBAUTHN).exists()
|
||||
|
||||
if not has_totp and not has_passkey:
|
||||
return Response(
|
||||
{"detail": "Two-factor authentication is not enabled"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -387,9 +510,21 @@ def regenerate_recovery_codes(request):
|
||||
recovery_instance = RecoveryCodes.activate(user)
|
||||
codes = recovery_instance.get_unused_codes()
|
||||
|
||||
# Log security event
|
||||
log_security_event(
|
||||
"recovery_codes_regenerated",
|
||||
request,
|
||||
user=user,
|
||||
metadata={"codes_generated": len(codes)},
|
||||
)
|
||||
|
||||
# Send security notification email
|
||||
send_security_notification(user, "recovery_codes_regenerated", {"codes_generated": len(codes)})
|
||||
|
||||
return Response(
|
||||
{
|
||||
"success": True,
|
||||
"recovery_codes": codes,
|
||||
"recovery_codes_count": len(codes),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user