diff --git a/djangoldp/__init__.py b/djangoldp/__init__.py index 0416a0bec7c1ef4e126ee732fc84c64aeaf1d6b9..e34d4cff350ca58512df3cc619e1cc6921252569 100644 --- a/djangoldp/__init__.py +++ b/djangoldp/__init__.py @@ -1,4 +1,4 @@ from django.db.models import options __version__ = '0.0.0' -options.DEFAULT_NAMES += ('lookup_field', 'rdf_type', 'rdf_context', 'auto_author', 'view_set', 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields', 'depth') +options.DEFAULT_NAMES += ('lookup_field', 'rdf_type', 'rdf_context', 'auto_author', 'view_set', 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields', 'depth', 'anonymous_perms', 'authenticated_perms', 'owner_perms') diff --git a/djangoldp/models.py b/djangoldp/models.py index ee4ebe72b4a8f759518654a818ec2340f6d047a1..477d8d942b26888d3a67f50722ea0d57a95d406e 100644 --- a/djangoldp/models.py +++ b/djangoldp/models.py @@ -5,6 +5,7 @@ from django.db.models.base import ModelBase from django.urls import get_resolver from django.utils.decorators import classonlymethod from guardian.shortcuts import get_perms +from djangoldp.permissions import LDPPermissions User._meta.rdf_type = "foaf:user" @@ -111,11 +112,8 @@ class Model(models.Model): @staticmethod def get_permissions(obj_or_model, user_or_group, filter): permissions = filter - for permission_class in Model.get_permission_classes(obj_or_model, []): + for permission_class in Model.get_permission_classes(obj_or_model, [LDPPermissions]): permissions = permission_class().filter_user_perms(user_or_group, obj_or_model, permissions) - - if not isinstance(user_or_group, AnonymousUser): - permissions += get_perms(user_or_group, obj_or_model) return [{'mode': {'@type': name.split('_')[0]}} for name in permissions] diff --git a/djangoldp/permissions.py b/djangoldp/permissions.py index b323c9925ec223df18d9fef36bb94ac2d983089b..888b563d6bc46db61fc6d376bad2e607de3d3088 100644 --- a/djangoldp/permissions.py +++ b/djangoldp/permissions.py @@ -1,156 +1,100 @@ -from guardian.shortcuts import get_objects_for_user -from rest_framework import filters -from rest_framework import permissions - -from djangoldp.models import Model - -""" -Liste des actions passées dans views selon le protocole REST : - list - create - retrieve - update, partial update - destroy -Pour chacune de ces actions, on va définir si on accepte la requête (True) ou non (False) -""" -""" - The instance-level has_object_permission method will only be called if the view-level has_permission - checks have already passed -""" - - -class WACPermissions(permissions.DjangoObjectPermissions): - perms_map = { - 'GET': ['%(app_label)s.view_%(model_name)s'], - 'OPTIONS': [], - 'HEAD': ['%(app_label)s.view_%(model_name)s'], - 'POST': ['%(app_label)s.add_%(model_name)s'], - 'PUT': ['%(app_label)s.change_%(model_name)s'], - 'PATCH': ['%(app_label)s.change_%(model_name)s'], - 'DELETE': ['%(app_label)s.delete_%(model_name)s'], - } - - def has_permission(self, request, view): - if request.method == 'OPTIONS': - return True - else: - return super().has_permission(request, view) +from rest_framework.permissions import BasePermission +from django.core.exceptions import PermissionDenied - # This method should be overriden by other permission classes - def user_permissions(self, user, obj): - return [] - - def filter_user_perms(self, user_or_group, obj, permissions): - return [perm for perm in permissions if perm in self.user_permissions(user_or_group, obj)] +class LDPPermissions(BasePermission): + """ + Default permissions + Anon: None + Auth: None but herit from Anon + Ownr: None but herit from Auth + """ + anonymous_perms = [] + authenticated_perms = ['inherit'] + owner_perms = ['inherit'] -class ObjectFilter(filters.BaseFilterBackend): - def filter_queryset(self, request, queryset, view): + def user_permissions(self, user, obj): """ - Ensure that queryset only contains objects visible by current user + Filter user permissions for a given object """ - perm = "view_{}".format(queryset.model._meta.model_name.lower()) - objects = get_objects_for_user(request.user, perm, klass=queryset) - return objects - - -class ObjectPermission(WACPermissions): - filter_class = ObjectFilter + # Get Anonymous permissions from Model's Meta. If not found use default + anonymous_perms = getattr(obj._meta, 'anonymous_perms', self.anonymous_perms) + # Get Auth permissions from Model's Meta. If not found use default + authenticated_perms = getattr(obj._meta, 'authenticated_perms', self.authenticated_perms) + # Extend Auth if inherit is given + if 'inherit' in authenticated_perms: + authenticated_perms = authenticated_perms + list(set(anonymous_perms) - set(authenticated_perms)) -class InboxPermissions(WACPermissions): - """ - Everybody can create - Author can edit - """ - anonymous_perms = ['create'] - authenticated_perms = ['create'] - author_perms = ['view', 'update', 'partial_update'] - - def has_permission(self, request, view): - if view.action in ['create']: - return True - else: - return super().has_permission(request, view) - - def has_object_permission(self, request, view, obj): - if view.action in ['update', 'partial_update', 'destroy']: - return False - else: - return super().has_object_permission(request, view, obj) + # Get Owner permissions from Model's Meta. If not found use default + owner_perms = getattr(obj._meta, 'owner_perms', self.owner_perms) + # Extend Owner if inherit is given + if 'inherit' in owner_perms: + owner_perms = owner_perms + list(set(authenticated_perms) - set(owner_perms)) - def user_permissions(self, user, obj): if user.is_anonymous(): - return self.anonymous_perms + return anonymous_perms + else: if hasattr(obj._meta, 'auto_author') and getattr(obj, Model.get_meta(obj, 'auto_author')) == user: - return self.author_perms - else: - return self.authenticated_perms - - -class AnonymousReadOnly(WACPermissions): - """ - Anonymous users: can read all posts - Logged in users: can read all posts + create new posts - Author: can read all posts + create new posts + update their own - """ + return owner_perms - anonymous_perms = ['view'] - authenticated_perms = ['view', 'add'] - author_perms = ['view', 'add', 'change', 'control', 'delete'] + else: + return authenticated_perms - def has_permission(self, request, view): - if view.action in ['list', 'retrieve']: - return True - elif view.action in ['create', 'update', 'partial_update'] and request.user.is_authenticated(): - return True - else: - return super().has_permission(request, view) + def filter_user_perms(self, user_or_group, obj, permissions): + # Only used on Model.get_permissions to translate permissions to LDP + return [perm for perm in permissions if perm in self.user_permissions(user_or_group, obj)] - def has_object_permission(self, request, view, obj): - if view.action == "create" and request.user.is_authenticated(): - return True - elif view.action in ["list", "retrieve"]: - return True - elif view.action in ['update', 'partial_update', 'destroy']: - if hasattr(obj._meta, 'auto_author') and getattr(obj, Model.get_meta(obj, 'auto_author')) == request.user: - return True - return super().has_object_permission(request, view, obj) - def user_permissions(self, user, obj): - if user.is_anonymous(): - return self.anonymous_perms - else: - if hasattr(obj._meta, 'auto_author') and getattr(obj, Model.get_meta(obj, 'auto_author')) == user: - return self.author_perms - else: - return self.authenticated_perms + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + def get_permissions(self, method, obj): + """ + Translate perms_map to request + """ + kwargs = { + 'app_label': obj._meta.app_label, + 'model_name': obj._meta.model_name + } -class LoggedReadOnly(WACPermissions): - """ - Anonymous users: Nothing - Logged in users: can read all posts - """ + # Only allows methods that are on perms_map + if method not in self.perms_map: + raise PermissionDenied - anonymous_perms = [] - authenticated_perms = ['view'] + return [perm % kwargs for perm in self.perms_map[method]] def has_permission(self, request, view): - if view.action in ['list', 'retrieve'] and request.user.is_authenticated(): - return True - else: - return super().has_permission(request, view) + """ + Access to containers + """ + perms = self.get_permissions(request.method, view.model) + # A bit tricky, but feels redondant to redeclarate perms_map + for perm in perms: + if not perm.split('.')[1].split('_')[0] in self.user_permissions(request.user, view.model): + return False + + return True def has_object_permission(self, request, view, obj): - if view.action in ["list", "retrieve"] and request.user.is_authenticated(): - return True - else: - return super().has_object_permission(request, view, obj) + """ + Access to objects + User have permission on request: Continue + User does not have permission: 403 + """ + perms = self.get_permissions(request.method, obj) - def user_permissions(self, user, obj): - if user.is_anonymous(): - return self.anonymous_perms - else: - return self.authenticated_perms + # A bit tricky, but feels redondant to redeclarate perms_map + for perm in perms: + if not perm.split('.')[1].split('_')[0] in self.user_permissions(request.user, obj): + return False + + return True diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 620516c057cb45c55be6a2396fd4bf3401eb27b5..06b38daad1f3c21a5b72e312f322631f2ff9ae2a 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -21,6 +21,7 @@ from rest_framework.utils.serializer_helpers import ReturnDict from djangoldp.fields import LDPUrlField, IdURLField from djangoldp.models import Model +from djangoldp.permissions import LDPPermissions class LDListMixin: @@ -251,7 +252,7 @@ class LDPSerializer(HyperlinkedModelSerializer): serializer_generator = LDPViewSet(model=model_class, lookup_field=Model.get_meta(model_class, 'lookup_field', 'pk'), permission_classes=Model.get_meta(model_class, - 'permission_classes', []), + 'permission_classes', [LDPPermissions]), fields=Model.get_meta(model_class, 'serializer_fields', []), nested_fields=Model.get_meta(model_class, 'nested_fields', [])) parent_depth = max(getattr(self.parent.Meta, "depth", 0) - 1, 0) diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py index 8bda453d3fa039d7e52302ffba65f3a5e766128c..823f2d18afcec6ff07023a93e7054064d8616277 100644 --- a/djangoldp/tests/models.py +++ b/djangoldp/tests/models.py @@ -4,7 +4,6 @@ from django.db import models from django.utils.datetime_safe import date from djangoldp.models import Model -from djangoldp.permissions import AnonymousReadOnly class Skill(Model): @@ -17,6 +16,9 @@ class Skill(Model): return self.joboffer_set.filter(date__gte=date.today()) class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] serializer_fields = ["@id", "title", "recent_jobs"] lookup_field = 'slug' @@ -31,6 +33,9 @@ class JobOffer(Model): return self.skills.filter(date__gte=date.today()) class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'change', 'add'] + owner_perms = ['inherit', 'delete', 'control'] nested_fields = ["skills"] serializer_fields = ["@id", "title", "skills", "recent_skills"] container_path = "job-offers/" @@ -42,12 +47,20 @@ class Conversation(models.Model): author_user = models.ForeignKey(settings.AUTH_USER_MODEL) peer_user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="peers_conv") + class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] + class UserProfile(Model): description = models.CharField(max_length=255, blank=True, null=True) user = models.OneToOneField(settings.AUTH_USER_MODEL) class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit'] + owner_perms = ['inherit', 'change', 'control'] depth = 1 @@ -56,15 +69,30 @@ class Message(models.Model): conversation = models.ForeignKey(Conversation, on_delete=models.DO_NOTHING) author_user = models.ForeignKey(settings.AUTH_USER_MODEL) + class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] + class Dummy(models.Model): some = models.CharField(max_length=255, blank=True, null=True) slug = models.SlugField(blank=True, null=True, unique=True) + class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] + class LDPDummy(Model): some = models.CharField(max_length=255, blank=True, null=True) + class Meta: + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] + class Invoice(Model): title = models.CharField(max_length=255, blank=True, null=True) @@ -72,7 +100,9 @@ class Invoice(Model): class Meta: depth = 2 - permission_classes = [AnonymousReadOnly] + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] nested_fields = ["batches"] @@ -82,6 +112,9 @@ class Batch(Model): class Meta: serializer_fields = ['@id', 'title', 'invoice', 'tasks'] + anonymous_perms = ['view', 'add'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] nested_fields = ["tasks", 'invoice'] @@ -91,6 +124,9 @@ class Task(models.Model): class Meta: serializer_fields = ['@id', 'title', 'batch'] + anonymous_perms = ['view'] + authenticated_perms = ['inherit', 'add'] + owner_perms = ['inherit', 'change', 'delete', 'control'] class Post(Model): @@ -100,6 +136,9 @@ class Post(Model): class Meta: auto_author = 'author' + anonymous_perms = ['view', 'add', 'delete', 'add', 'change', 'control'] + authenticated_perms = ['inherit'] + owner_perms = ['inherit'] get_user_model()._meta.serializer_fields = ['@id', 'username', 'first_name', 'last_name', 'email', 'userprofile', 'conversation_set',] diff --git a/djangoldp/tests/runner.py b/djangoldp/tests/runner.py index 296716a1aeb175693462c90389eaa903b5905796..740948df72e9c93b96148e6343fb5717df2186a3 100644 --- a/djangoldp/tests/runner.py +++ b/djangoldp/tests/runner.py @@ -2,7 +2,8 @@ import django import sys from django.conf import settings -settings.configure(DEBUG=True, +settings.configure(DEBUG=False, + ALLOWED_HOSTS = ["*"], DATABASES={ 'default': { 'ENGINE': 'django.db.backends.sqlite3', diff --git a/djangoldp/tests/tests_anonymous_permissions.py b/djangoldp/tests/tests_anonymous_permissions.py index f1ace746ec4e04039ec774f329ba300f90b5c1e0..adfb255ec8da827528b4848f412b3b95c8e97f24 100644 --- a/djangoldp/tests/tests_anonymous_permissions.py +++ b/djangoldp/tests/tests_anonymous_permissions.py @@ -1,53 +1,38 @@ -from django.contrib.auth.models import AnonymousUser -from django.test import TestCase -from rest_framework.test import APIRequestFactory +import json -from guardian.shortcuts import get_anonymous_user +from django.test import TestCase +from rest_framework.test import APIClient -from djangoldp.permissions import AnonymousReadOnly +from djangoldp.permissions import LDPPermissions from djangoldp.tests.models import JobOffer from djangoldp.views import LDPViewSet -import json - class TestAnonymousUserPermissions(TestCase): def setUp(self): - self.factory = APIRequestFactory() - self.user = get_anonymous_user() - self.job = JobOffer.objects.create(title="job") + self.client = APIClient(enforce_csrf_checks=True) + self.job = JobOffer.objects.create(title="job", slug=1) def test_get_request_for_anonymousUser(self): - request = self.factory.get("/job-offers/") - request.user = self.user - my_view = LDPViewSet.as_view({'get': 'list'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request) + response = self.client.get('/job-offers/') + self.assertEqual(response.status_code, 200) + + def test_get_1_request_for_anonymousUser(self): + response = self.client.get('/job-offers/1/') self.assertEqual(response.status_code, 200) def test_post_request_for_anonymousUser(self): - data = {'title': 'new idea'} - request = self.factory.post('/job-offers/', json.dumps(data), content_type='application/ld+json') - my_view = LDPViewSet.as_view({'post': 'create'}, model=JobOffer, nested_fields=["skills"], permission_classes=[AnonymousReadOnly]) - response = my_view(request, pk=1) + post = {'title': "job_created"} + response = self.client.post('/job-offers/', data=json.dumps(post), content_type='application/ld+json') self.assertEqual(response.status_code, 403) def test_put_request_for_anonymousUser(self): - request = self.factory.put("/job-offers/") - my_view = LDPViewSet.as_view({'put': 'update'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request, pk=self.job.pk) + body = {'title':"job_updated"} + response = self.client.put('/job-offers/{}/'.format(self.job.pk), data=json.dumps(body), + content_type='application/ld+json') self.assertEqual(response.status_code, 403) - + def test_patch_request_for_anonymousUser(self): - request = self.factory.patch("/job-offers/") - my_view = LDPViewSet.as_view({'patch': 'partial_update'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request, pk=self.job.pk) + response = self.client.patch('/job-offers/' + str(self.job.pk) + "/", + content_type='application/ld+json') self.assertEqual(response.status_code, 403) diff --git a/djangoldp/tests/tests_user_permissions.py b/djangoldp/tests/tests_user_permissions.py index 5bda0e5880cc96337b74e0344cf8f3f087e882d8..cd66a481941dfa416696bb9f30111404bb08b7aa 100644 --- a/djangoldp/tests/tests_user_permissions.py +++ b/djangoldp/tests/tests_user_permissions.py @@ -1,50 +1,40 @@ from django.contrib.auth.models import User -from rest_framework.test import APIRequestFactory, APIClient, APITestCase +from rest_framework.test import APIClient, APITestCase -from djangoldp.permissions import AnonymousReadOnly +from djangoldp.permissions import LDPPermissions from .models import JobOffer from djangoldp.views import LDPViewSet import json - class TestUserPermissions(APITestCase): def setUp(self): - self.factory = APIRequestFactory() - self.client = APIClient() - self.user = User.objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') - self.job = JobOffer.objects.create(title="job") - - def tearDown(self): - self.user.delete() + user = User.objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') + self.client = APIClient(enforce_csrf_checks=True) + self.client.force_authenticate(user=user) + self.job = JobOffer.objects.create(title="job", slug=1) def test_get_for_authenticated_user(self): - request = self.factory.get('/job-offers/') - request.user = self.user - my_view = LDPViewSet.as_view({'get': 'list'}, model=JobOffer, permission_classes=[AnonymousReadOnly]) - response = my_view(request) + response = self.client.get('/job-offers/') + self.assertEqual(response.status_code, 200) + + def test_get_1_for_authenticated_user(self): + response = self.client.get('/job-offers/1/') self.assertEqual(response.status_code, 200) def test_post_request_for_authenticated_user(self): - data = {'title': 'new idea'} - request = self.factory.post('/job-offers/', json.dumps(data), content_type='application/ld+json') - request.user = self.user - my_view = LDPViewSet.as_view({'post': 'create'}, model=JobOffer, nested_fields=["skills"], permission_classes=[AnonymousReadOnly]) - response = my_view(request, pk=1) + post = {'title': "job_created"} + response = self.client.post('/job-offers/', data=json.dumps(post), content_type='application/ld+json') self.assertEqual(response.status_code, 201) - # def test_put_request_for_authenticated_user(self): - # data = {'title':"job_updated"} - # request = self.factory.put('/job-offers/' + str(self.job.pk) + "/", data) - # request.user = self.user - # my_view = LDPViewSet.as_view({'put': 'update'}, model=JobOffer, permission_classes=[AnonymousReadOnly]) - # response = my_view(request, pk=self.job.pk) - # self.assertEqual(response.status_code, 200) - # - # def test_request_patch_for_authenticated_user(self): - # request = self.factory.patch('/job-offers/' + str(self.job.pk) + "/") - # request.user = self.user - # my_view = LDPViewSet.as_view({'patch': 'partial_update'}, model=JobOffer, permission_classes=[AnonymousReadOnly]) - # response = my_view(request, pk=self.job.pk) - # self.assertEqual(response.status_code, 200) \ No newline at end of file + def test_put_request_for_authenticated_user(self): + body = {'title':"job_updated"} + response = self.client.put('/job-offers/{}/'.format(self.job.pk), data=json.dumps(body), + content_type='application/ld+json') + self.assertEqual(response.status_code, 200) + + def test_request_patch_for_authenticated_user(self): + response = self.client.patch('/job-offers/' + str(self.job.pk) + "/", + content_type='application/ld+json') + self.assertEqual(response.status_code, 200) diff --git a/djangoldp/urls.py b/djangoldp/urls.py index fb01b36f4e228c0b59f08e58bd2868a0877200a0..31aa83129a4d34905f582f9c25b4a92f7761701b 100644 --- a/djangoldp/urls.py +++ b/djangoldp/urls.py @@ -5,6 +5,7 @@ from django.conf.urls import url, include from djangoldp.models import LDPSource, Model from djangoldp.views import LDPSourceViewSet +from djangoldp.permissions import LDPPermissions def __clean_path(path): @@ -34,7 +35,7 @@ for class_name in model_classes: urlpatterns.append(url(r'^' + path, include( urls_fct(model=model_class, lookup_field=Model.get_meta(model_class, 'lookup_field', 'pk'), - permission_classes=Model.get_meta(model_class, 'permission_classes', []), + permission_classes=Model.get_meta(model_class, 'permission_classes', [LDPPermissions]), fields=Model.get_meta(model_class, 'serializer_fields', []), nested_fields=Model.get_meta(model_class, 'nested_fields', []))))) diff --git a/djangoldp/views.py b/djangoldp/views.py index a6e342c21707aafdbb880e44f9ff3298b324e332..addeb0ff552be2bf6fa116222e06eadd63b62aa4 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -18,6 +18,7 @@ from rest_framework.response import Response from djangoldp.models import LDPSource, Model +from djangoldp.permissions import LDPPermissions class JSONLDRenderer(JSONRenderer): @@ -255,7 +256,7 @@ class LDPNestedViewSet(LDPViewSet): parent_lookup_field=cls.get_lookup_arg(**kwargs), model_prefix=cls.get_model(**kwargs)._meta.object_name.lower(), permission_classes=Model.get_permission_classes(related_field.related_model, - kwargs.get('permission_classes', ())), + kwargs.get('permission_classes', [LDPPermissions])), lookup_url_kwarg=related_field.related_model._meta.object_name.lower() + '_id')