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
9 changes: 7 additions & 2 deletions cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import edx_api_doc_tools as apidocs
from django.conf import settings
from organizations import api as org_api
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser

from openedx.core.lib.api.view_utils import view_auth_classes

Expand Down Expand Up @@ -99,11 +102,13 @@ def get(self, request: Request):
return Response(serializer.data)


@view_auth_classes(is_authenticated=True)
class HomePageCoursesView(APIView):
"""
View for getting all courses and libraries available to the logged in user.
"""
authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser)
permission_classes = (IsAuthenticated,)
serializer_class = CourseHomeTabSerializer
@apidocs.schema(
parameters=[
apidocs.string_parameter(
Expand Down Expand Up @@ -170,7 +175,7 @@ def get(self, request: Request):
"archived_courses": archived_courses,
"in_process_course_actions": in_process_course_actions,
}
serializer = CourseHomeTabSerializer(courses_context)
serializer = self.serializer_class(courses_context)
return Response(serializer.data)


Expand Down
53 changes: 52 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import ddt
import pytz
from django.conf import settings
from django.test import override_settings
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework.test import APIClient
from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_content import api as content_api
from organizations.tests.factories import OrganizationFactory
Expand All @@ -18,6 +19,7 @@
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.modulestore_migrator import api as migrator_api
from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.content_libraries import api as lib_api

Expand Down Expand Up @@ -402,3 +404,52 @@ def test_home_page_libraries_response(self):

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(expected_response, response.json())


class HomePageCoursesViewPermissionsTest(TestCase):
"""
ADR 0026 – permission regression tests for HomePageCoursesView.

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:v1: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.

HomePageCoursesView 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