|
| 1 | +"""Handler for security events.""" |
| 2 | + |
| 3 | +import logging |
| 4 | +from datetime import datetime |
| 5 | + |
| 6 | +from django.utils import timezone |
| 7 | + |
| 8 | +from tdpservice.security.models import SecurityEventToken, SecurityEventType |
| 9 | +from tdpservice.users.models import User |
| 10 | + |
| 11 | +logger = logging.getLogger(__name__) |
| 12 | + |
| 13 | + |
| 14 | +class SecurityEventHandler: |
| 15 | + """Handler for security events.""" |
| 16 | + |
| 17 | + def _handle_unknown_event(security_event): |
| 18 | + """Handle unimplemented or unknown events.""" |
| 19 | + logger.warning(f"No handler for event type: {security_event.event_type}") |
| 20 | + security_event.event_type = SecurityEventType.UNKNOWN_EVENT |
| 21 | + |
| 22 | + def _handle_account_disabled(security_event): |
| 23 | + """Handle account-disabled event.""" |
| 24 | + user = security_event.user |
| 25 | + user.is_active = False |
| 26 | + user.save() |
| 27 | + logger.info(f"User {user.username} account disabled due to Login.gov event") |
| 28 | + |
| 29 | + def _handle_account_enabled(security_event): |
| 30 | + """Handle account-enabled event.""" |
| 31 | + user = security_event.user |
| 32 | + user.is_active = True |
| 33 | + user.save() |
| 34 | + logger.info(f"User {user.username} account re-enabled due to Login.gov event") |
| 35 | + |
| 36 | + def _handle_account_purged(security_event): |
| 37 | + """Handle account-purged event when a user deletes their Login.gov account.""" |
| 38 | + # Find user by Login.gov UUID |
| 39 | + user = security_event.user |
| 40 | + |
| 41 | + # Set login_gov_uuid to null and deactivate user |
| 42 | + user.login_gov_uuid = None |
| 43 | + user.is_active = False |
| 44 | + user.save() |
| 45 | + |
| 46 | + logger.info( |
| 47 | + f"User {user.username} Login.gov account was purged. Prepared for potential recreation." |
| 48 | + ) |
| 49 | + |
| 50 | + def _handle_mfa_locked(security_event): |
| 51 | + """Handle mfa-locked event.""" |
| 52 | + user = security_event.user |
| 53 | + logger.info( |
| 54 | + f"User {user.username} account locked due to multiple MFA failures." |
| 55 | + ) |
| 56 | + |
| 57 | + def _get_emails(security_event): |
| 58 | + """Get the old and new emails from the event data.""" |
| 59 | + event_data = security_event.event_data |
| 60 | + subject = event_data.get("subject") |
| 61 | + new_email = subject.get("subject_type") |
| 62 | + old_email = subject.get("email") |
| 63 | + return new_email, old_email |
| 64 | + |
| 65 | + def _handle_email_changed(security_event): |
| 66 | + """Handle email-changed event.""" |
| 67 | + new_email, old_email = SecurityEventHandler._get_emails(security_event) |
| 68 | + |
| 69 | + user = security_event.user |
| 70 | + user.email = new_email |
| 71 | + user.username = new_email |
| 72 | + user.save() |
| 73 | + logger.info(f"User changed email from {old_email} to {new_email}") |
| 74 | + |
| 75 | + def _handle_email_recycled(security_event): |
| 76 | + """Handle email-recycled event.""" |
| 77 | + new_email, old_email = SecurityEventHandler._get_emails(security_event) |
| 78 | + user = security_event.user |
| 79 | + logger.info(f"User {user.username} recycled extra email address {old_email}") |
| 80 | + |
| 81 | + def _handle_password_reset(security_event): |
| 82 | + """Handle password-reset event.""" |
| 83 | + user = security_event.user |
| 84 | + logger.info(f"User {user.username} performed a password reset") |
| 85 | + |
| 86 | + def _handle_recovery_activated(security_event): |
| 87 | + """Handle recovery-activated event when user initiates account recovery.""" |
| 88 | + user = security_event.user |
| 89 | + logger.info(f"User {user.username} initiated account recovery") |
| 90 | + |
| 91 | + def _handle_recovery_information_changed(security_event): |
| 92 | + """Handle recovery-information-changed event when user modifies their recovery methods.""" |
| 93 | + user = security_event.user |
| 94 | + logger.info(f"User {user.username} changed their recovery information") |
| 95 | + |
| 96 | + def _handle_reproof_complete(security_event): |
| 97 | + """Handle reproof-complete event when user completes account recovery.""" |
| 98 | + user = security_event.user |
| 99 | + logger.info(f"User {user.username} completed account re-verification.") |
| 100 | + |
| 101 | + handler_map = { |
| 102 | + SecurityEventType.ACCOUNT_DISABLED: _handle_account_disabled, |
| 103 | + SecurityEventType.ACCOUNT_ENABLED: _handle_account_enabled, |
| 104 | + SecurityEventType.ACCOUNT_PURGED: _handle_account_purged, |
| 105 | + SecurityEventType.MFA_LOCKED: _handle_mfa_locked, |
| 106 | + SecurityEventType.EMAIL_CHANGED: _handle_email_changed, |
| 107 | + SecurityEventType.EMAIL_RECYCLED: _handle_email_recycled, |
| 108 | + SecurityEventType.PASSWORD_RESET: _handle_password_reset, |
| 109 | + SecurityEventType.RECOVERY_ACTIVATED: _handle_recovery_activated, |
| 110 | + SecurityEventType.RECOVERY_INFORMATION_CHANGED: _handle_recovery_information_changed, |
| 111 | + SecurityEventType.REPROOF_COMPLETE: _handle_reproof_complete, |
| 112 | + } |
| 113 | + |
| 114 | + @classmethod |
| 115 | + def _get_user(cls, subject): |
| 116 | + """Get User model from email or UUID.""" |
| 117 | + if "sub" in subject: |
| 118 | + user_qset = User.objects.filter(login_gov_uuid=subject.get("sub")) |
| 119 | + if user_qset.exists() and user_qset.count() == 1: |
| 120 | + return user_qset.first() |
| 121 | + else: |
| 122 | + raise ValueError( |
| 123 | + "No user found with login_gov_uuid: {}".format(subject.get("sub")) |
| 124 | + ) |
| 125 | + elif "email" in subject: |
| 126 | + # Check both emails in the subject to see if we have the user |
| 127 | + user_qset = User.objects.filter(username=subject.get("email")) |
| 128 | + if user_qset.exists() and user_qset.count() == 1: |
| 129 | + return user_qset.first() |
| 130 | + |
| 131 | + user_qset = User.objects.filter(username=subject.get("subject_type")) |
| 132 | + if user_qset.exists() and user_qset.count() == 1: |
| 133 | + return user_qset.first() |
| 134 | + |
| 135 | + raise ValueError( |
| 136 | + "No user found with the provided 'email' or 'subject_type' in subject of security event." |
| 137 | + ) |
| 138 | + |
| 139 | + raise ValueError("No user info found in subject of security event.") |
| 140 | + |
| 141 | + @classmethod |
| 142 | + def handle_event(cls, event_type, event_data, decoded_jwt): |
| 143 | + """Handle specific event types.""" |
| 144 | + try: |
| 145 | + subject = event_data.get("subject", {}) |
| 146 | + user = cls._get_user(subject) |
| 147 | + |
| 148 | + # Convert Unix timestamp to datetime if present |
| 149 | + iat_timestamp = decoded_jwt.get("iat") |
| 150 | + issued_at = None |
| 151 | + if iat_timestamp: |
| 152 | + try: |
| 153 | + issued_at = datetime.fromtimestamp(iat_timestamp, tz=timezone.utc) |
| 154 | + except (ValueError, TypeError) as e: |
| 155 | + logger.warning(f"Error converting timestamp {iat_timestamp}: {e}") |
| 156 | + |
| 157 | + security_event = SecurityEventToken.objects.create( |
| 158 | + user=user, |
| 159 | + email=user.email, |
| 160 | + event_type=event_type, |
| 161 | + event_data=event_data, |
| 162 | + jwt_id=decoded_jwt.get("jti"), |
| 163 | + issuer=decoded_jwt.get("iss"), |
| 164 | + issued_at=issued_at, |
| 165 | + ) |
| 166 | + |
| 167 | + # Call the appropriate handler |
| 168 | + handler = cls.handler_map.get(event_type, cls._handle_unknown_event) |
| 169 | + handler(security_event) |
| 170 | + |
| 171 | + security_event.processed = True |
| 172 | + security_event.processed_at = timezone.now() |
| 173 | + security_event.save() |
| 174 | + |
| 175 | + except Exception as e: |
| 176 | + logger.exception(f"Error handling event {event_type}: {e}") |
0 commit comments