From cd46203d55cee17c78f7404e3c32b65b50b77557 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Thu, 15 Oct 2020 23:41:38 +0200 Subject: [PATCH] added permission classes for sharing + tests --- cookbook/helper/permission_helper.py | 32 +++++++++++++++++++++++++ cookbook/tests/api/test_api_shopping.py | 27 +++++++++++++++++++++ cookbook/views/api.py | 15 ++++++------ cookbook/views/lists.py | 3 ++- 4 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 cookbook/tests/api/test_api_shopping.py diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index 6ff5be08..5c2a381c 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -72,6 +72,23 @@ def is_object_owner(user, obj): return False +def is_object_shared(user, obj): + """ + Tests if a given user is shared for a given object + test performed by checking user against the objects shared table + superusers bypass all checks, unauthenticated users cannot own anything + :param user django auth user object + :param obj any object that should be tested + :return: true if user is shared for object, false otherwise + """ + # TODO this could be improved/cleaned up by adding share checks for relevant objects + if not user.is_authenticated: + return False + if user.is_superuser: + return True + return user in obj.shared.all() + + def share_link_valid(recipe, share): """ Verifies the validity of a share uuid @@ -147,6 +164,21 @@ class CustomIsOwner(permissions.BasePermission): return is_object_owner(request.user, obj) +class CustomIsShared(permissions.BasePermission): # TODO function duplicate name + """ + Custom permission class for django rest framework views + verifies user is shared for the object he is trying to access + """ + message = _('You cannot interact with this object as its not owned by you!') + + def has_permission(self, request, view): + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + print("called is shared") + return is_object_shared(request.user, obj) + + class CustomIsGuest(permissions.BasePermission): """ Custom permission class for django rest framework views diff --git a/cookbook/tests/api/test_api_shopping.py b/cookbook/tests/api/test_api_shopping.py new file mode 100644 index 00000000..cb902bb4 --- /dev/null +++ b/cookbook/tests/api/test_api_shopping.py @@ -0,0 +1,27 @@ +import json + +from django.contrib import auth +from django.db.models import ProtectedError +from django.urls import reverse + +from cookbook.models import Storage, Sync, Keyword, ShoppingList +from cookbook.tests.views.test_views import TestViews + + +class TestApiShopping(TestViews): + + def setUp(self): + super(TestApiShopping, self).setUp() + self.list_1 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_1)) + self.list_2 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_2)) + + def test_shopping_view_permissions(self): + self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 404), (self.admin_client_1, 404), (self.superuser_client, 200)], + reverse('api:shoppinglist-detail', args={self.list_1.id})) + + self.list_1.shared.add(auth.get_user(self.user_client_2)) + + self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 200), (self.admin_client_1, 404), (self.superuser_client, 200)], + reverse('api:shoppinglist-detail', args={self.list_1.id})) + + # TODO add tests for editing diff --git a/cookbook/views/api.py b/cookbook/views/api.py index e3a2a154..86ad5ebd 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -26,7 +26,7 @@ from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser from rest_framework.response import Response from rest_framework.viewsets import ViewSetMixin -from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare +from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare, CustomIsShared from cookbook.helper.recipe_url_import import get_from_html from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit, SyncLog, ShoppingListRecipe, ShoppingList, ShoppingListEntry from cookbook.provider.dropbox import Dropbox @@ -155,7 +155,7 @@ class MealPlanViewSet(viewsets.ModelViewSet): """ queryset = MealPlan.objects.all() serializer_class = MealPlanSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAuthenticated] # TODO fix permissions def get_queryset(self): queryset = MealPlan.objects.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).distinct().all() @@ -244,7 +244,7 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): class ShoppingListRecipeViewSet(viewsets.ModelViewSet): queryset = ShoppingListRecipe.objects.all() serializer_class = ShoppingListRecipeSerializer - permission_classes = [CustomIsUser] # TODO add custom validation + permission_classes = [CustomIsUser, ] # TODO add custom validation # TODO custom get qs @@ -252,7 +252,7 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet): class ShoppingListEntryViewSet(viewsets.ModelViewSet): queryset = ShoppingListEntry.objects.all() serializer_class = ShoppingListEntrySerializer - permission_classes = [CustomIsOwner] # TODO add custom validation + permission_classes = [CustomIsOwner, ] # TODO add custom validation # TODO custom get qs @@ -260,11 +260,12 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet): class ShoppingListViewSet(viewsets.ModelViewSet): queryset = ShoppingList.objects.all() serializer_class = ShoppingListSerializer - permission_classes = [CustomIsOwner] + permission_classes = [CustomIsOwner | CustomIsShared] def get_queryset(self): - queryset = self.queryset.filter(created_by=self.request.user).all() - return queryset + if self.request.user.is_superuser: + return self.queryset + return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).all() def get_serializer_class(self): autosync = self.request.query_params.get('autosync', None) diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index 1c272362..034ddd9d 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -1,6 +1,7 @@ from datetime import datetime from django.contrib.auth.decorators import login_required +from django.db.models import Q from django.db.models.functions import Lower from django.shortcuts import render from django.utils.translation import gettext as _ @@ -49,7 +50,7 @@ def food(request): @group_required('user') def shopping_list(request): - f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(created_by=request.user).all().order_by('finished', 'created_at')) + f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).all().order_by('finished', 'created_at')) table = ShoppingListTable(f.qs) RequestConfig(request, paginate={'per_page': 25}).configure(table)