Skip to content
Open
Show file tree
Hide file tree
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
11 changes: 7 additions & 4 deletions cms/djangoapps/contentstore/rest_api/v2/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=[
Expand Down Expand Up @@ -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,
})
Expand Down
52 changes: 52 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v2/views/tests/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
Loading