diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index f338fd510177..412c62edc3f6 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -7,6 +7,7 @@ import datetime import logging +import re from functools import wraps import pytz @@ -1070,7 +1071,7 @@ class LMSAccountRetirementView(ViewSet): parser_classes = (JSONParser,) @request_requires_username - def post(self, request): + def post(self, request): # pylint: disable=too-many-statements """ POST /api/user/v1/accounts/retire_misc/ @@ -1114,30 +1115,97 @@ def post(self, request): record_exception() return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: - try: - user_id = retirement.user.id - except AttributeError: - user_id = 'unknown' + user_id, log_error = self._store_retirement_error(exc, retirement, "RetirementStateError") + log.error( 'RetirementStateError during user retirement: user_id=%s, error=%s', - user_id, str(exc) + user_id, log_error ) record_exception() - return Response(str(exc), status=status.HTTP_400_BAD_REQUEST) + return Response("RetirementStateError occurred during retirement", status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except - try: - user_id = retirement.user.id - except AttributeError: - user_id = 'unknown' + user_id, log_error = self._store_retirement_error(exc, retirement) + log.error( 'Unexpected error during user retirement: user_id=%s, error=%s', - user_id, str(exc) + user_id, log_error ) record_exception() - return Response(str(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response("Internal error occurred during retirement", status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT) + def _sanitize_error_message(self, error_message): + """ + Remove common PII from error messages while preserving debugging context. + + Args: + error_message (str): The original error message + + Returns: + str: Error message with PII removed + """ + if not error_message: + return error_message + + message = error_message + + # Remove email addresses + message = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b', + '-', message, flags=re.IGNORECASE) + + # Remove username values in various formats + message = re.sub(r"username='[^']*'", "username='-'", message) + message = re.sub(r'username="[^"]*"', 'username="-"', message) + message = re.sub(r'username:\s*[^\s,]+', 'username: -', message) + message = re.sub(r'username=\s*[^\s,]+', 'username=-', message) + + # Remove common username patterns in error messages + message = re.sub(r'\bUser\s+[A-Za-z0-9._-]+\s+not found', 'User - not found', message, flags=re.IGNORECASE) + message = re.sub(r'for user\s+[A-Za-z0-9._-]+', 'for user -', message, flags=re.IGNORECASE) + + return message + + def _store_retirement_error(self, exc, retirement, error_prefix=""): + """ + Store sanitized error information in retirement status and return user_id and log_error for logging. + + Args: + exc: The exception object + retirement: The retirement object (may be None) + error_prefix: Optional prefix for the error message (e.g., "RetirementStateError") + + Returns: + tuple: (user_id, log_error) for logging purposes + """ + # Get user_id safely + try: + user_id = retirement.user.id if retirement else 'unknown' + except AttributeError: + user_id = 'unknown' + + # Sanitize error message + log_error = self._sanitize_error_message(str(exc)) + + # Create error message with prefix + if error_prefix: + error_msg = f"{error_prefix}: {log_error}" + else: + error_msg = f"{type(exc).__name__}: {log_error}" + + # Store error information in retirement status as plain text + try: + if retirement is not None: + if retirement.responses: + retirement.responses += f"\n{error_msg}" + else: + retirement.responses = error_msg + retirement.save() + except AttributeError as e: + log.warning('Failed to store error in retirement status: %s', str(e)) + + return user_id, log_error + class AccountRetirementView(ViewSet): """