diff --git a/openedx/core/djangoapps/notifications/permissions.py b/openedx/core/djangoapps/notifications/permissions.py new file mode 100644 index 0000000000..8aa4703789 --- /dev/null +++ b/openedx/core/djangoapps/notifications/permissions.py @@ -0,0 +1,27 @@ +""" +Permissions for notifications +""" +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from rest_framework.permissions import IsAuthenticated + +from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser + + +def allow_any_authenticated_user(): + """ + Function and class decorator that abstracts the authentication and permission checks for api views. + Allows both verified and non-verified users + """ + def _decorator(func_or_class): + """ + Requires either OAuth2 or Session-based authentication. + """ + func_or_class.authentication_classes = ( + JwtAuthentication, + BearerAuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser + ) + func_or_class.permission_classes = (IsAuthenticated,) + return func_or_class + return _decorator diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index d423eb7dfe..88fd9619ef 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -91,11 +91,11 @@ class CourseEnrollmentListViewTest(ModuleStoreTestCase): def test_course_enrollment_api_permission(self): """ Calls api without login. - Check is 403 is returned + Check is 401 is returned """ url = reverse('enrollment-list') response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) @@ -241,7 +241,7 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): Test get user notification preference without login. """ response = self.client.get(self.path) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @mock.patch("eventtracking.tracker.emit") def test_get_user_notification_preference(self, mock_emit): @@ -411,13 +411,13 @@ class NotificationListAPIViewTest(APITestCase): def test_list_notifications_without_authentication(self): """ - Test that the view returns 403 if the user is not authenticated. + Test that the view returns 401 if the user is not authenticated. """ # Make a request to the view without authenticating. response = self.client.get(self.url) # Assert that the response is unauthorized. - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_list_notifications_with_expiry_date(self): """ @@ -533,10 +533,10 @@ class NotificationCountViewSetTestCase(ModuleStoreTestCase): def test_get_unseen_notifications_count_for_unauthenticated_user(self): """ - Test that the endpoint returns 403 for an unauthenticated user. + Test that the endpoint returns 401 for an unauthenticated user. """ response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_get_unseen_notifications_count_for_user_with_no_notifications(self): """ diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 1b72da965a..6d82f67f01 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -9,7 +9,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from opaque_keys.edx.keys import CourseKey from pytz import UTC -from rest_framework import generics, permissions, status +from rest_framework import generics, status from rest_framework.generics import UpdateAPIView from rest_framework.response import Response from rest_framework.views import APIView @@ -19,6 +19,7 @@ from openedx.core.djangoapps.notifications.models import ( CourseNotificationPreference, get_course_notification_preference_config_version ) +from openedx.core.djangoapps.notifications.permissions import allow_any_authenticated_user from .base_notification import COURSE_NOTIFICATION_APPS from .config.waffle import ENABLE_NOTIFICATIONS @@ -38,6 +39,7 @@ from .serializers import ( from .utils import get_show_notifications_tray +@allow_any_authenticated_user() class CourseEnrollmentListView(generics.ListAPIView): """ API endpoint to get active CourseEnrollments for requester. @@ -67,7 +69,6 @@ class CourseEnrollmentListView(generics.ListAPIView): - 403: The requester cannot access resource. """ serializer_class = NotificationCourseEnrollmentSerializer - permission_classes = [permissions.IsAuthenticated] def get_paginated_response(self, data): """ @@ -105,6 +106,7 @@ class CourseEnrollmentListView(generics.ListAPIView): }) +@allow_any_authenticated_user() class UserNotificationPreferenceView(APIView): """ Supports retrieving and patching the UserNotificationPreference @@ -141,7 +143,6 @@ class UserNotificationPreferenceView(APIView): } } """ - permission_classes = (permissions.IsAuthenticated,) def get(self, request, course_key_string): """ @@ -220,6 +221,7 @@ class UserNotificationPreferenceView(APIView): return Response(serializer.data, status=status.HTTP_200_OK) +@allow_any_authenticated_user() class NotificationListAPIView(generics.ListAPIView): """ API view for listing notifications for a user. @@ -253,7 +255,6 @@ class NotificationListAPIView(generics.ListAPIView): """ serializer_class = NotificationSerializer - permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): """ @@ -279,13 +280,12 @@ class NotificationListAPIView(generics.ListAPIView): ).order_by('-id') +@allow_any_authenticated_user() class NotificationCountView(APIView): """ API view for getting the unseen notifications count and show_notification_tray flag for a user. """ - permission_classes = (permissions.IsAuthenticated,) - def get(self, request): """ Get the unseen notifications count and show_notification_tray flag for a user. @@ -334,13 +334,12 @@ class NotificationCountView(APIView): }) +@allow_any_authenticated_user() class MarkNotificationsSeenAPIView(UpdateAPIView): """ API view for marking user's all notifications seen for a provided app_name. """ - permission_classes = (permissions.IsAuthenticated,) - def update(self, request, *args, **kwargs): """ Marks all notifications for the given app name seen for the authenticated user. @@ -368,13 +367,12 @@ class MarkNotificationsSeenAPIView(UpdateAPIView): return Response({'message': _('Notifications marked as seen.')}, status=200) +@allow_any_authenticated_user() class NotificationReadAPIView(APIView): """ API view for marking user notifications as read, either all notifications or a single notification """ - permission_classes = (permissions.IsAuthenticated,) - def patch(self, request, *args, **kwargs): """ Marks all notifications or single notification read for the given