diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/home.py b/cms/djangoapps/contentstore/rest_api/v2/views/home.py index 9d37684bfd81..4f875215baa1 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/home.py @@ -2,12 +2,13 @@ import edx_api_doc_tools as apidocs from collections import OrderedDict +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.request import Request from rest_framework.views import APIView from rest_framework.pagination import PageNumberPagination - -from openedx.core.lib.api.view_utils import view_auth_classes +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from cms.djangoapps.contentstore.utils import get_course_context_v2 from cms.djangoapps.contentstore.rest_api.v2.serializers import CourseHomeTabSerializerV2 @@ -42,9 +43,11 @@ def paginate_queryset(self, queryset, request, view=None): return super().paginate_queryset(queryset, request, view) -@view_auth_classes(is_authenticated=True) class HomePageCoursesViewV2(APIView): """View for getting all courses available to the logged in user.""" + authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser) + permission_classes = (IsAuthenticated,) + serializer_class = CourseHomeTabSerializerV2 @apidocs.schema( parameters=[ @@ -140,7 +143,7 @@ def get(self, request: Request): self.request, view=self ) - serializer = CourseHomeTabSerializerV2({ + serializer = self.serializer_class({ 'courses': courses_page, 'in_process_course_actions': in_process_course_actions, }) diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py index 6a51610ac9f2..44631f74e934 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py @@ -8,11 +8,14 @@ import ddt import pytz from django.conf import settings +from django.test import TestCase from django.urls import reverse from rest_framework import status +from rest_framework.test import APIClient from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import reverse_course_url +from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory @@ -298,3 +301,52 @@ def test_if_empty_list_of_courses_non_staff(self, query_param, value): self.assertEqual(len(response.data["results"]["courses"]), 0) self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class HomePageCoursesViewV2PermissionsTest(TestCase): + """ + ADR 0026 – permission regression tests for HomePageCoursesViewV2. + + Verifies that the explicit permission_classes = (IsAuthenticated,) enforces + the same access rules previously set by the @view_auth_classes(is_authenticated=True) + decorator. + """ + + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("cms.djangoapps.contentstore:v2:courses") + self.user = UserFactory.create() + self.staff_user = UserFactory.create(is_staff=True) + + def test_unauthenticated_request_returns_401(self): + """ + Unauthenticated request (no credentials) must be rejected with 401. + + Before ADR 0026: enforced by @view_auth_classes(is_authenticated=True). + After ADR 0026: enforced by permission_classes = (IsAuthenticated,). + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_authenticated_user_gets_200(self): + """ + Any authenticated user (not necessarily staff) must receive 200. + + HomePageCoursesViewV2 only requires authentication — no staff role needed. + The view returns an empty course list for users with no assigned courses. + """ + self.client.force_authenticate(user=self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_staff_user_gets_200(self): + """Staff user must also receive 200 (staff is a superset of authenticated).""" + self.client.force_authenticate(user=self.staff_user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_post_by_unauthenticated_returns_401(self): + """Non-GET methods also enforce authentication — POST without credentials is 401.""" + response = self.client.post(self.url, data={}) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)