Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 81 additions & 13 deletions openedx/core/djangoapps/user_api/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import datetime
import logging
import re
from functools import wraps

import pytz
Expand Down Expand Up @@ -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/

Expand Down Expand Up @@ -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")
Comment on lines 1117 to +1118
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retirement variable may not be defined when this exception handler is reached. If the error occurs on line 1090 (UserRetirementStatus.get_retirement_for_retirement_action(username)), the retirement variable will not exist yet, causing a NameError when passed to _store_retirement_error(). Consider initializing retirement = None before the try block.

Copilot uses AI. Check for mistakes.

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)
Comment on lines +1160 to +1161
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for username removal uses [^\s,]+ which matches one or more non-whitespace, non-comma characters. This might be too broad and could match parts of other words or values. For example, in a message like "Error processing username=john123 for operation", this could match "john123" correctly, but in "Error: username=john123failed", it might incorrectly match "john123failed". Consider using more specific patterns or adding word boundaries where appropriate.

Suggested change
message = re.sub(r'username:\s*[^\s,]+', 'username: -', message)
message = re.sub(r'username=\s*[^\s,]+', 'username=-', message)
message = re.sub(r'\busername:\s*[A-Za-z0-9._-]+\b', 'username: -', message)
message = re.sub(r'\busername=\s*[A-Za-z0-9._-]+\b', 'username=-', message)

Copilot uses AI. Check for mistakes.

# 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))
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning log message passes the exception object through str(e) without sanitizing it. While this is an AttributeError that's unlikely to contain PII, it's inconsistent with the goal of sanitizing all error messages. Since this error is related to retirement status operations, it could potentially contain references to user data. Consider sanitizing this error message as well for consistency.

Suggested change
log.warning('Failed to store error in retirement status: %s', str(e))
sanitized_error = self._sanitize_error_message(str(e))
log.warning('Failed to store error in retirement status: %s', sanitized_error)

Copilot uses AI. Check for mistakes.

return user_id, log_error
Comment on lines +1138 to +1207
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error handling logic with PII sanitization and error storage lacks test coverage. The existing test file (test_retirement_views.py) has tests for the LMS retirement endpoint but doesn't cover the new sanitization logic, error storage in retirement.responses, or the new generic error messages. Consider adding tests to verify that PII is properly sanitized from various error message formats, that errors are correctly stored in the retirement status, and that generic error messages are returned to API callers.

Copilot uses AI. Check for mistakes.


class AccountRetirementView(ViewSet):
"""
Expand Down
Loading