From c26d21b04bc6b7185e54c168528b05285a5f5dae Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Thu, 15 Apr 2021 17:14:28 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Properly=20check=20permissions=20fo?= =?UTF-8?q?r=20Projects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sophon/core/models.py | 38 +++++++++++++++++++++----------------- sophon/core/permissions.py | 18 ++++++++++++++++++ sophon/core/views.py | 12 ++++++------ 3 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 sophon/core/permissions.py diff --git a/sophon/core/models.py b/sophon/core/models.py index 32ba696..ca5c6af 100644 --- a/sophon/core/models.py +++ b/sophon/core/models.py @@ -9,19 +9,6 @@ import json import abc -class RelatedToProject(metaclass=abc.ABCMeta): - """ - A model which is related to :class:`.Project` in some way. - """ - - @abc.abstractmethod - def get_project(self) -> "Project": - """ - :return: The project this object is related to. - """ - raise NotImplementedError() - - class DataSource(models.Model): """ A :class:`.DataSource` is a web service which provides access to statistical information sourced by multiple data @@ -312,15 +299,12 @@ class DataFlow(models.Model): return f"[{self.datasource}] {self.id}" -class Project(models.Model, RelatedToProject): +class Project(models.Model): """ A research :class:`.Project` is a work which may use zero or more :class:`.DataSource`\\ s to prove or disprove an hypothesis. """ - def get_project(self): - return self - slug = models.SlugField( "Slug", help_text="Unique alphanumeric string which identifies the project.", @@ -349,6 +333,7 @@ class Project(models.Model, RelatedToProject): ("PRIVATE", "🔒 Private"), ], default="INTERNAL", + max_length=16, ) owner = models.ForeignKey( @@ -371,5 +356,24 @@ class Project(models.Model, RelatedToProject): blank=True, ) + def get_contributors(self): + return {self.owner, *self.collaborators.values()} + + def can_be_viewed_by(self, user): + if self.visibility == "PUBLIC": + return True + elif self.visibility == "INTERNAL": + return not user.is_anonymous() + elif self.visibility == "PRIVATE": + return user in self.get_contributors() + else: + raise ValueError(f"Unknown visibility value: {self.visibility}") + + def can_be_edited_by(self, user): + return user in self.get_contributors() + + def can_be_administrated_by(self, user): + return user == self.owner + def __str__(self): return self.slug diff --git a/sophon/core/permissions.py b/sophon/core/permissions.py new file mode 100644 index 0000000..035146d --- /dev/null +++ b/sophon/core/permissions.py @@ -0,0 +1,18 @@ +import logging +from rest_framework import permissions + +log = logging.getLogger(__name__) + + +class ProjectPermissions(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if view.action == "retrieve": + return obj.can_be_viewed_by(request.user) + elif view.action == "update": + return obj.can_be_edited_by(request.user) + elif view.action == "partial_update": + return obj.can_be_edited_by(request.user) + elif view.action == "destroy": + return obj.can_be_administrated_by(request.user) + log.warning(f"Rejecting permissions for unknown action: {view.action}") + return False diff --git a/sophon/core/views.py b/sophon/core/views.py index abcbb1c..23b3cfe 100644 --- a/sophon/core/views.py +++ b/sophon/core/views.py @@ -1,5 +1,5 @@ -from rest_framework import viewsets, decorators, response -from . import models, serializers +from rest_framework import viewsets, decorators, response, permissions +from . import models, serializers, permissions as custom_permissions from datetime import datetime from logging import getLogger @@ -12,7 +12,7 @@ class ProjectViewSet(viewsets.ModelViewSet): """ queryset = models.Project.objects.all() serializer_class = serializers.ProjectSerializer - permission_classes = [] + permission_classes = [custom_permissions.ProjectPermissions] class DataFlowViewSet(viewsets.ModelViewSet): @@ -21,7 +21,7 @@ class DataFlowViewSet(viewsets.ModelViewSet): """ queryset = models.DataFlow.objects.all() serializer_class = serializers.DataFlowSerializer - permission_classes = [] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] class DataSourceViewSet(viewsets.ModelViewSet): @@ -30,10 +30,10 @@ class DataSourceViewSet(viewsets.ModelViewSet): """ queryset = models.DataSource.objects.all() serializer_class = serializers.DataSourceSerializer - permission_classes = [] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] @decorators.action(methods=["post"], detail=True) - def sync(self, request, pk): + def sync(self, request, *args, **kwargs): """ Syncronize the :class:`.models.DataFlow`\\ s with the ones stored in the server of the :class:`.models.DataSource`\\ .