diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index b701f543..1837f1a2 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -206,6 +206,14 @@ class CustomIsOwner(permissions.BasePermission): return is_object_owner(request.user, obj) +class CustomIsOwnerReadOnly(CustomIsOwner): + def has_permission(self, request, view): + return super().has_permission(request, view) and request.method in SAFE_METHODS + + def has_object_permission(self, request, view, obj): + return super().has_object_permission(request, view) and request.method in SAFE_METHODS + + class CustomIsSpaceOwner(permissions.BasePermission): """ Custom permission class for django rest framework views @@ -214,7 +222,7 @@ class CustomIsSpaceOwner(permissions.BasePermission): message = _('You cannot interact with this object as it is not owned by you!') def has_permission(self, request, view): - return request.user.is_authenticated + return request.user.is_authenticated and request.space.created_by == request.user def has_object_permission(self, request, view, obj): return is_space_owner(request.user, obj) diff --git a/cookbook/tests/api/test_api_invitelinke.py b/cookbook/tests/api/test_api_invitelinke.py index 0c228e84..b91cb10f 100644 --- a/cookbook/tests/api/test_api_invitelinke.py +++ b/cookbook/tests/api/test_api_invitelinke.py @@ -2,11 +2,13 @@ import json import pytest from django.contrib import auth +from django.contrib.auth.models import AnonymousUser from django.db.models import OuterRef, Subquery from django.urls import reverse from django_scopes import scopes_disabled -from cookbook.models import Ingredient, Step, InviteLink +from cookbook.helper.permission_helper import switch_user_active_space +from cookbook.models import Ingredient, Step, InviteLink, UserSpace LIST_URL = 'api:invitelink-list' DETAIL_URL = 'api:invitelink-detail' @@ -17,7 +19,7 @@ DETAIL_URL = 'api:invitelink-detail' ['g1_s1', 403, 0], ['u1_s1', 403, 0], ['a1_s1', 200, 1], - ['a2_s1', 200, 0], + ['a2_s1', 403, 0], ]) def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1): space_1.created_by = auth.get_user(a1_s1) @@ -38,7 +40,7 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1): ['a1_s1', 200], ['g1_s2', 403], ['u1_s2', 403], - ['a1_s2', 404], + ['a1_s2', 403], ]) def test_update(arg, request, space_1, u1_s1, a1_s1): with scopes_disabled(): @@ -75,6 +77,7 @@ def test_add(arg, request, a1_s1, space_1): space_1.created_by = auth.get_user(a1_s1) space_1.save() c = request.getfixturevalue(arg[0]) + r = c.post( reverse(LIST_URL), {'group': {'id': 3, 'name': 'admin'}}, @@ -107,7 +110,7 @@ def test_delete(u1_s1, u1_s2, a1_s1, a2_s1, space_1): args={il.id} ) ) - assert r.status_code == 404 + assert r.status_code == 403 # owner can delete r = a1_s1.delete( diff --git a/cookbook/tests/api/test_api_space.py b/cookbook/tests/api/test_api_space.py index bd9e326d..6ade2cfd 100644 --- a/cookbook/tests/api/test_api_space.py +++ b/cookbook/tests/api/test_api_space.py @@ -29,20 +29,23 @@ def test_list_permission(arg, request, space_1, a1_s1): assert len(json.loads(result.content)) == arg[2] -def test_list_permission_owner(u1_s1, space_1): - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0 +def test_list_permission_owner(u1_s1, a1_s1, space_1): + space_1.created_by = auth.get_user(a1_s1) + space_1.save() + assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1 + assert u1_s1.get(reverse(LIST_URL)).status_code == 403 space_1.created_by = auth.get_user(u1_s1) space_1.save() - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + assert u1_s1.get(reverse(LIST_URL)).status_code == 403 @pytest.mark.parametrize("arg", [ ['a_u', 403], - ['g1_s1', 404], - ['u1_s1', 404], + ['g1_s1', 403], + ['u1_s1', 403], ['a1_s1', 200], - ['g1_s2', 404], - ['u1_s2', 404], + ['g1_s2', 403], + ['u1_s2', 403], ['a1_s2', 404], ]) def test_update(arg, request, space_1, a1_s1): @@ -66,8 +69,8 @@ def test_update(arg, request, space_1, a1_s1): @pytest.mark.parametrize("arg", [ ['a_u', 403], - ['g1_s1', 405], - ['u1_s1', 405], + ['g1_s1', 403], + ['u1_s1', 403], ['a1_s1', 405], ]) def test_add(arg, request, u1_s2): @@ -90,7 +93,7 @@ def test_delete(u1_s1, u1_s2, a1_s1, space_1): args={space_1.id} ) ) - assert r.status_code == 405 + assert r.status_code == 403 # event the space owner cannot delete his space over the api (this might change later but for now it's only available in the UI) r = a1_s1.delete( @@ -99,4 +102,4 @@ def test_delete(u1_s1, u1_s2, a1_s1, space_1): args={space_1.id} ) ) - assert r.status_code == 204 + assert r.status_code == 405 diff --git a/cookbook/tests/api/test_api_userspace.py b/cookbook/tests/api/test_api_userspace.py index deccd7ed..9856f570 100644 --- a/cookbook/tests/api/test_api_userspace.py +++ b/cookbook/tests/api/test_api_userspace.py @@ -78,9 +78,9 @@ def test_update_space_owner(a1_s1, space_1): @pytest.mark.parametrize("arg", [ ['a_u', 403], - ['g1_s1', 405], - ['u1_s1', 405], - ['a1_s1', 405], + ['g1_s1', 403], + ['u1_s1', 403], + ['a1_s1', 403], ]) def test_add(arg, request, u1_s1, space_1): c = request.getfixturevalue(arg[0]) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index bf2b072b..7e07afbd 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -45,7 +45,7 @@ from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner, CustomIsShare, CustomIsShared, CustomIsUser, - group_required, CustomIsSpaceOwner, switch_user_active_space, is_space_owner) + group_required, CustomIsSpaceOwner, switch_user_active_space, is_space_owner, CustomIsOwnerReadOnly) from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper @@ -386,7 +386,7 @@ class SpaceViewSet(viewsets.ModelViewSet): class UserSpaceViewSet(viewsets.ModelViewSet): queryset = UserSpace.objects serializer_class = UserSpaceSerializer - permission_classes = [CustomIsSpaceOwner] + permission_classes = [CustomIsSpaceOwner | CustomIsOwnerReadOnly] http_method_names = ['get', 'patch', 'delete'] def destroy(self, request, *args, **kwargs):