Commit 1f765698 by Clinton Blackburn Committed by Clinton Blackburn

Added permissions and filtering to Catalog resource

- Permissions are required to view an individual catalog
- The list view only shows catalogs accessible to the authenticated user

ECOM-3973
parent 380514e0
from dry_rest_permissions.generics import DRYPermissionFiltersBase
from guardian.shortcuts import get_objects_for_user
class PermissionsFilter(DRYPermissionFiltersBase):
def filter_list_queryset(self, request, queryset, view):
""" Filters the list queryset, returning only the objects accessible by the user. """
perm = queryset.model.get_permission('view')
return get_objects_for_user(request.user, perm)
# pylint: disable=redefined-builtin
# pylint: disable=redefined-builtin,no-member
import urllib
import ddt
......@@ -10,7 +10,7 @@ from course_discovery.apps.api.tests.jwt_utils import generate_jwt_header_for_us
from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin, OAuth2Mixin
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.tests.factories import CourseFactory
......@@ -22,7 +22,7 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
def setUp(self):
super(CatalogViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.client.force_authenticate(self.user)
self.catalog = CatalogFactory(query='title:abc*')
self.course = CourseFactory(key='a/b/c', title='ABC Test Course')
self.refresh_index()
......@@ -43,6 +43,12 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
self.assertEqual(catalog.name, name)
self.assertEqual(catalog.query, query)
def grant_catalog_permission_to_user(self, user, action):
""" Grant the user access to view `self.catalog`. """
perm = '{action}_catalog'.format(action=action)
user.add_obj_perm(perm, self.catalog)
self.assertTrue(user.has_perm('catalogs.' + perm, self.catalog))
def test_create_without_authentication(self):
""" Verify authentication is required when creating, updating, or deleting a catalog. """
self.client.logout()
......@@ -152,3 +158,58 @@ class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixi
catalog = Catalog.objects.get(id=self.catalog.id)
self.assertEqual(catalog.name, name)
self.assertEqual(catalog.query, query)
def test_retrieve_permissions(self):
""" Verify only users with the correct permissions can create, read, or modify a Catalog. """
# Use an unprivileged user
user = UserFactory(is_staff=False, is_superuser=False)
self.client.force_authenticate(user)
url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id})
# A user with no permissions should NOT be able to view a Catalog.
self.assertFalse(user.has_perm('catalogs.view_catalog', self.catalog))
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
# The permitted user should be able to view the Catalog.
self.grant_catalog_permission_to_user(user, 'view')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_list_permissions(self):
""" Verify only catalogs accessible to the user are returned in the list view. """
user = UserFactory(is_staff=False, is_superuser=False)
self.client.force_authenticate(user)
url = reverse('api:v1:catalog-list')
# An user with no permissions should not see any catalogs
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data['results'], [])
# The client should be able to see permissions for which it has access
self.grant_catalog_permission_to_user(user, 'view')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data['results'], self.serialize_catalog([self.catalog], many=True))
def test_write_permissions(self):
""" Verify only authorized users can update or delete Catalogs. """
url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id})
user = UserFactory(is_staff=False, is_superuser=False)
self.client.force_authenticate(user)
# Unprivileged users cannot modify Catalogs
response = self.client.put(url)
self.assertEqual(response.status_code, 403)
response = self.client.delete(url)
self.assertEqual(response.status_code, 403)
# With the right permissions, the user can perform the specified actions
self.grant_catalog_permission_to_user(user, 'change')
response = self.client.patch(url, {'query': '*:*'})
self.assertEqual(response.status_code, 200)
self.grant_catalog_permission_to_user(user, 'delete')
response = self.client.delete(url)
self.assertEqual(response.status_code, 204)
import logging
from django.db.models.functions import Lower
from dry_rest_permissions.generics import DRYPermissions
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from course_discovery.apps.api.filters import PermissionsFilter
from course_discovery.apps.api.serializers import CatalogSerializer, CourseSerializer, ContainedCoursesSerializer
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.constants import COURSE_ID_REGEX
......@@ -18,7 +20,9 @@ logger = logging.getLogger(__name__)
class CatalogViewSet(viewsets.ModelViewSet):
""" Catalog resource. """
filter_backends = (PermissionsFilter,)
lookup_field = 'id'
permission_classes = (DRYPermissions,)
queryset = Catalog.objects.all()
serializer_class = CatalogSerializer
......
......@@ -3,10 +3,11 @@ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from haystack.query import SearchQuerySet
from course_discovery.apps.core.mixins import ModelPermissionsMixin
from course_discovery.apps.course_metadata.models import Course
class Catalog(TimeStampedModel):
class Catalog(ModelPermissionsMixin, TimeStampedModel):
name = models.CharField(max_length=255, null=False, blank=False, help_text=_('Catalog name'))
query = models.TextField(null=False, blank=False, help_text=_('Query to retrieve catalog contents'))
......
from dry_rest_permissions.generics import allow_staff_or_superuser, authenticated_users
class ModelPermissionsMixin:
""" Adds DRY permissions to a model.
Inheriting models should have the default add, change, and delete permissions, as well as the
custom "view" permission.
"""
@classmethod
def get_permission(cls, action):
"""
Returns a permission name for the given class and action.
Arguments:
action (str): Action tied to the desired permission (e.g. add, change, delete).
Returns:
str: Permission
"""
kwargs = {
'app_label': cls._meta.app_label,
'model_name': cls._meta.model_name,
'action': action
}
return '{app_label}.{action}_{model_name}'.format(**kwargs)
@staticmethod
@authenticated_users
def has_read_permission(_request):
return True
@staticmethod
@authenticated_users
@allow_staff_or_superuser
def has_write_permission(_request):
# This is only here to get past the global has_permission check. The object permissions will determine
# if a specific instance can be updated.
return True
@classmethod
def has_create_permission(cls, request):
user = request.user
perm = cls.get_permission('add')
return user.is_staff or user.is_superuser or user.has_perm(perm)
@authenticated_users
@allow_staff_or_superuser
def has_object_destroy_permission(self, request):
perm = self.get_permission('delete')
return request.user.has_perm(perm, self)
@authenticated_users
@allow_staff_or_superuser
def has_object_read_permission(self, request):
perm = self.get_permission('view')
return request.user.has_perm(perm, self)
@authenticated_users
@allow_staff_or_superuser
def has_object_update_permission(self, request):
perm = self.get_permission('change')
return request.user.has_perm(perm, self)
......@@ -37,6 +37,7 @@ THIRD_PARTY_APPS = (
'simple_history',
'haystack',
'guardian',
'dry_rest_permissions',
)
PROJECT_APPS = (
......
......@@ -8,6 +8,7 @@ django-waffle==0.11
djangorestframework==3.3.1
djangorestframework-jwt==1.7.2
django-rest-swagger[reST]==0.3.4
dry-rest-permissions==0.1.6
edx-auth-backends==0.1.3
edx-drf-extensions==0.2.0
edx-rest-api-client==1.5.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment