From 849096cc3ba7939a5a5d30d886cb51e2dd39eb30 Mon Sep 17 00:00:00 2001 From: kingoftech-v01 Date: Fri, 10 Apr 2026 02:50:25 +0000 Subject: [PATCH] fix(enrollment): block inactive users from enrolling in courses Users who have not activated their account via email verification can currently enroll in courses. This adds an is_active check in CourseEnrollment.enroll() when check_access=True. Closes #36402 --- .../student/models/course_enrollment.py | 8 +++ .../student/tests/test_enrollment.py | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/common/djangoapps/student/models/course_enrollment.py b/common/djangoapps/student/models/course_enrollment.py index 4b61608b6294..7c821cec4c95 100644 --- a/common/djangoapps/student/models/course_enrollment.py +++ b/common/djangoapps/student/models/course_enrollment.py @@ -742,6 +742,14 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal raise NonExistentCourseError # lint-amnesty, pylint: disable=raise-missing-from # noqa: B904 if check_access: + if not user.is_active: + log.warning( + "User %s failed to enroll in course %s because the account has not been activated.", + user.username, + str(course_key), + ) + raise EnrollmentNotAllowed("Account must be activated before enrollment.") + if cls.is_enrollment_closed(user, course) and not can_upgrade: log.warning( "User %s failed to enroll in course %s because enrollment is closed (can_upgrade=%s).", diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index 767479ac2756..99f9d8d9ef41 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -17,6 +17,7 @@ CourseEnrollment, CourseFullError, EnrollmentClosedError, + EnrollmentNotAllowed, ) from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory @@ -521,3 +522,54 @@ def test_score_recalculation_on_enrollment_update(self): countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE, kwargs=local_task_args ) + + def test_unit_inactive_user_enrollment_blocked(self): + """ + Unit test: CourseEnrollment.enroll() should raise EnrollmentNotAllowed + when user.is_active is False and check_access is True. + Bug #36402. + """ + self.user.is_active = False + self.user.save() + with pytest.raises(EnrollmentNotAllowed): + CourseEnrollment.enroll(self.user, self.course.id, check_access=True) + + def test_integration_inactive_user_cannot_enroll_active_can(self): + """ + Integration test: An inactive user is blocked from enrollment, but after + activation the same user can enroll successfully. + Bug #36402. + """ + self.user.is_active = False + self.user.save() + with pytest.raises(EnrollmentNotAllowed): + CourseEnrollment.enroll(self.user, self.course.id, check_access=True) + + self.user.is_active = True + self.user.save() + enrollment = CourseEnrollment.enroll(self.user, self.course.id, check_access=True) + assert enrollment.is_active + assert CourseEnrollment.is_enrolled(self.user, self.course.id) + + def test_bug_36402_regression_inactive_user_enrollment(self): + """ + Regression test for Bug #36402: Non-active users can enroll in courses. + Before the fix, an inactive (unverified email) user could enroll freely. + After the fix, EnrollmentNotAllowed is raised when check_access=True. + """ + self.user.is_active = False + self.user.save() + with pytest.raises(EnrollmentNotAllowed, match="activated"): + CourseEnrollment.enroll(self.user, self.course.id, check_access=True) + + def test_inactive_user_enrollment_bypasses_check_when_no_access_check(self): + """ + Server-to-server enrollment (check_access=False) should still allow + inactive user enrollment, since the caller is trusted. + Bug #36402 — ensures the fix does not break server-side enrollment. + """ + self.user.is_active = False + self.user.save() + enrollment = CourseEnrollment.enroll(self.user, self.course.id, check_access=False) + assert enrollment.is_active + assert CourseEnrollment.is_enrolled(self.user, self.course.id)