From c059b88101c3afc9df61c0cbb4bebe8939688abf Mon Sep 17 00:00:00 2001 From: yann Date: Fri, 6 Jun 2025 08:28:45 +0200 Subject: [PATCH] refactor permissions --- softdesk/authentication/serializers.py | 7 + softdesk/support/admin.py | 2 +- softdesk/support/models.py | 4 + softdesk/support/permissions.py | 10 +- softdesk/support/serializers.py | 30 +++- softdesk/support/views.py | 208 +++++++++++++++++++------ 6 files changed, 205 insertions(+), 56 deletions(-) diff --git a/softdesk/authentication/serializers.py b/softdesk/authentication/serializers.py index d71346e..7401b76 100644 --- a/softdesk/authentication/serializers.py +++ b/softdesk/authentication/serializers.py @@ -15,6 +15,13 @@ class UserSerializer(ModelSerializer): 'can_data_be_shared'] +class UserListSerializer(ModelSerializer): + + class Meta: + model = User + fields = ['id', 'username'] + + class UserUpdateSerializer(ModelSerializer): class Meta: diff --git a/softdesk/support/admin.py b/softdesk/support/admin.py index d7c3623..8060963 100644 --- a/softdesk/support/admin.py +++ b/softdesk/support/admin.py @@ -7,7 +7,7 @@ class AdminProject(admin.ModelAdmin): @admin.display(description='contributors') def contributors(self, obj): - return obj.contributor + return obj.contributors class AdminIssue(admin.ModelAdmin): list_display = ('id', 'title', 'author', 'project') diff --git a/softdesk/support/models.py b/softdesk/support/models.py index 4aa4f76..df020f0 100644 --- a/softdesk/support/models.py +++ b/softdesk/support/models.py @@ -39,9 +39,13 @@ class ProjectContributor(models.Model): class Meta: unique_together = ('contributor', 'project') + def get_user(self): + return self.contributor + def __str__(self): return self.contributor.username + class Issue(models.Model): class Priority(models.TextChoices): diff --git a/softdesk/support/permissions.py b/softdesk/support/permissions.py index 3dc9f42..a0a1a06 100644 --- a/softdesk/support/permissions.py +++ b/softdesk/support/permissions.py @@ -1,5 +1,6 @@ from rest_framework.permissions import BasePermission -from support.models import Project +from support.models import Project, Issue, Comment + class IsAuthor(BasePermission): @@ -13,7 +14,8 @@ class IsAuthor(BasePermission): class IsContributor(BasePermission): def has_object_permission(self, request, view, object): - return bool(request.user - and request.user.is_authenticated + print(object.contributors.all()) + return bool(request.user.is_authenticated and request.user in object.contributors.all() - ) \ No newline at end of file + ) + diff --git a/softdesk/support/serializers.py b/softdesk/support/serializers.py index 1560f15..21ecacf 100644 --- a/softdesk/support/serializers.py +++ b/softdesk/support/serializers.py @@ -15,12 +15,15 @@ class ContributorSerializer(ModelSerializer): class ContributorListSerializer(ModelSerializer): + contributor = StringRelatedField(many=False) + class Meta: model = ProjectContributor fields = ['contributor'] + class ProjectSerializer(ModelSerializer): author = StringRelatedField(many=False) @@ -65,13 +68,9 @@ class IssueSerializer(ModelSerializer): model = Issue fields = ['id', 'title', 'project', 'date_created', 'priority', 'tag', 'status', 'author'] - read_only_field = ['author'] - def validate_author(self, data): - if Project.objects.filter(contributors=data.author).exists(): - raise ValidationError("Requestor isn't contributor") - return data + def validate_project(self, data): # if data['user'] not in data['project'].contributors: @@ -84,15 +83,34 @@ class IssueSerializer(ModelSerializer): return data +class IssueDetailSerializer(ModelSerializer): + + comments = SerializerMethodField() + author = StringRelatedField(many=False) + + + class Meta: + model = Issue + fields = ['title', 'project', 'date_created', 'priority', + 'tag', 'status', 'author', 'comments'] + + def get_comments(self, instance): + queryset = Comment.objects.filter(issue=instance.id) + serializer = CommentListSerializer(queryset, many=True) + return serializer.data + + class CommentListSerializer(ModelSerializer): issue = IssueSerializer(many=False) + class Meta: model = Comment fields = ['title', 'date_created', 'author', 'issue'] + class CommentDetailSerializer(ModelSerializer): class Meta: model = Comment - fields = ['title', 'date_created', 'author', 'description'] \ No newline at end of file + fields = ['title', 'date_created', 'description', 'issue', 'author'] diff --git a/softdesk/support/views.py b/softdesk/support/views.py index 8955e40..7194de4 100644 --- a/softdesk/support/views.py +++ b/softdesk/support/views.py @@ -1,14 +1,19 @@ +from django.contrib.auth.checks import check_models_permissions from django.shortcuts import render from rest_framework.serializers import raise_errors_on_nested_writes from rest_framework.viewsets import ModelViewSet +from rest_framework.views import APIView from support.models import Project, ProjectContributor, Issue, Comment from authentication.models import User from support.serializers import (ProjectSerializer, ProjectDetailSerializer, ContributorSerializer, + ContributorListSerializer, IssueSerializer, + IssueDetailSerializer, CommentListSerializer, CommentDetailSerializer) +from authentication.serializers import UserListSerializer from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import (IsAuthenticated, @@ -16,6 +21,7 @@ from rest_framework.permissions import (IsAuthenticated, from support.permissions import IsAuthor, IsContributor from rest_framework.decorators import action from rest_framework.exceptions import ValidationError +from django.core.exceptions import PermissionDenied class ProjectViewSet(ModelViewSet): @@ -26,10 +32,30 @@ class ProjectViewSet(ModelViewSet): queryset = Project.objects.filter(active=True) - def get_serializer_class(self): - if self.action == 'retrieve': - return self.detail_serializer_class - return super().get_serializer_class() + + def retrieve(self, request, pk, *args, **kwargs): + """ + check if requestor is in the project's contributor + Raises exception or returns project detail + """ + if not request.user in Project.objects.get(id=pk).contributors.all(): + raise PermissionDenied() + queryset = Project.objects.get(id=pk) + return Response(ProjectDetailSerializer(queryset).data) + + def partial_update(self, request, pk, *args, **kwargs): + """ + check if requestor is author + then save changes and returns project details + """ + if not request.user == Project.objects.get(id=pk).author: + raise PermissionDenied() + queryset = Project.objects.get(id=pk) + serialized = ProjectDetailSerializer(queryset, data=request.data, partial=True) + if serialized.is_valid(raise_exception=True): + serialized.save() + return Response(serialized.data) + def perform_create(self, serializer): """set authenticated user as author and contributor on creation""" @@ -39,65 +65,107 @@ class ProjectViewSet(ModelViewSet): if contributor_serializer.is_valid(): contributor_serializer.save() - @action(detail=True, methods=['patch'], - permission_classes=[IsContributor], - basename='add_contributor') - def add_contributor(self, request, pk): - """Create the user/project contributor's relation""" - if 'contributor' in request.data: - contributor = User.objects.get(username=request.data['contributor']) - data = {'contributor': contributor.id, 'project': pk} - serializer = ContributorSerializer(data=data) - if serializer.is_valid(): - serializer.save() - return Response(f"User {contributor} added", - status=status.HTTP_202_ACCEPTED) - return Response("This user is already contributing", - status=status.HTTP_226_IM_USED) - return Response(f"Key error;`contributor` is expected, " - f"not `{list(request.data)[0]}`", - status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['get'], permission_classes=[IsContributor]) + def test(self, request, pk): + """only for testing purpose, should be deleted not published""" + if not request.user in Project.objects.get(id=pk).contributors.all(): + raise PermissionDenied() + return Response("OK") + + @action(detail=True, methods=['patch'], permission_classes=[IsContributor]) + def contributor(self, request, pk): + """Add a contributor to a project + by creating a ProjectContributor's instance + """ + #check if requestor is contributor + if not request.user in Project.objects.get(id=pk).contributors.all(): + raise PermissionDenied() + + if request.data is None or not 'contributor' in request.data: + return Response(f"Key error;`contributor` is expected", + status=status.HTTP_400_BAD_REQUEST) + #get the user's instance + contributor = User.objects.get(username=request.data['contributor']) + data = {'contributor': contributor.id, 'project': int(pk)} + serializer = ContributorSerializer(data=data) + project = Project.objects.get(id=pk) + if serializer.is_valid(): + serializer.save() + return Response(f"User {contributor} " + f"added to project {project}", + status=status.HTTP_202_ACCEPTED) + return Response("This user is already contributing", + status=status.HTTP_226_IM_USED) + class IssueViewSet(ModelViewSet): - permission_classes = [IsContributor] + permission_classes = [IsAuthenticatedOrReadOnly] serializer_class = IssueSerializer + detail_serializer_class = IssueDetailSerializer + + def get_serializer_class(self): + if self.action == 'retrieve': + return self.detail_serializer_class + return super().get_serializer_class() def get_queryset(self): - #check for the right query string or return nothing + """ + returns only the issues related to projects + where requestor is contributor or empty list + """ if self.request.GET.get('project'): project_id = int(self.request.GET.get('project')) - project = Project.objects.get(id=project_id) - self.check_object_permissions(self.request, project) + if not self.request.user in Project.objects.get( + id=project_id).contributors.all(): + raise PermissionDenied() return Issue.objects.filter(project=project_id) + projects = Project.objects.filter(contributors=self.request.user).values('id') + #query on a list + return Issue.objects.filter(project__in=projects) - def get_contributors(self, project): - queryset = ProjectContributor.objects.filter(project=project) - contributors_serializer = ContributorSerializer(queryset, many=True) - return contributors_serializer.data + def perform_update(self, serializer): + if self.request.user == serializer.author: + return Response("OK") + if serializer.is_valid(raise_exception=True): + serializer.save(author=author) + return Response(serializer.data) + return Response("Data error", status=status.HTTP_400_BAD_REQUEST) + + + + @action(detail=True, methods=['get']) + def contributors(self, request, pk): + """ + check if requestor is contributor then returns the list + of the contributors to the issue's project or raise unauthorized + """ + issue = Issue.objects.get(id=pk) + if ProjectContributor.objects.filter(project=issue.project).filter(contributor=request.user): + return Response(UserListSerializer(issue.project.contributors.all(), many=True).data) + else: + raise PermissionDenied() def create(self, request, *args, **kwargs): - print(request.data['project']) + if not 'project' in request.data: + return Response("Need project") project = Project.objects.get(id=request.data['project']) serializer = IssueSerializer(data=request.data) - - - print(request.data['project'], type(request.data['project'])) - print(self.get_contributors(request.data['project'])) - - if self.request.user in project.contributors: - if serializer.is_valid(raise_exception=True): - serializer.author = self.request.user - serializer.save() - response = { - "message": f"Issue created for project {project}", - "data": serializer.data - } - return Response(response, status = status.HTTP_201_CREATED) + if self.request.user not in project.contributors.all(): + return Response("Requestor isn't contributor for this project", + status=status.HTTP_403_FORBIDDEN) + if serializer.is_valid(raise_exception=True): + serializer.save(author=self.request.user) + response = { + "message": f"Issue created for project {project}", + "data": serializer.data + } + return Response(response, status = status.HTTP_201_CREATED) #def perform_create(self, serializer): # """set authenticated user as author and contributor on creation""" @@ -105,17 +173,67 @@ class IssueViewSet(ModelViewSet): class ContributorViewSet(ModelViewSet): + permission_classes = [IsAuthenticatedOrReadOnly] + serializer_class = ContributorSerializer queryset = ProjectContributor.objects.all() + def get_queryset(self): + if self.request.GET.get('project'): + serializer_class = ContributorListSerializer + return (ProjectContributor.objects. + filter(project=self.request.GET.get('project'))) + elif self.request.GET.get('user'): + user_id = User.objects.get(username=self.request.GET.get('user')).id + return (ProjectContributor.objects. + filter(contributor=user_id)) + return self.queryset + class CommentViewSet(ModelViewSet): + permission_classes = [IsAuthenticatedOrReadOnly] + serializer_class = CommentListSerializer detail_serializer_class = CommentDetailSerializer queryset = Comment.objects.all() + def get_queryset(self): + """returns only comments associated with issue where the requestor + is project's contributor + """ + if self.request.GET.get('issue'): + issue_id = int(self.request.GET.get('issue')) + print(issue_id, type(issue_id)) + project = Issue.objects.get(id=issue_id).project + if not self.request.user in Issue.objects.get( + id=issue_id).project.contributors.all(): + raise PermissionDenied() + return Comment.objects.filter(issue=issue_id) + if self.request.data: + return Comment.objects.filter(issue=self.request.data['issue']) + + + def get_serializer_class(self): if self.action == 'retrieve': return self.detail_serializer_class return super().get_serializer_class() + + + + def create(self, request, *args, **kwargs): + user = request.user + issue = Issue.objects.get(id=request.data['issue']) + project = issue.project + if issue.project.contributors.filter(username=request.user.username): + serializer = CommentDetailSerializer(data=request.data) + if serializer.is_valid(raise_exception=True): + #serializer.author = request.user.username + serializer.save(author=request.user.username) + response = {"message": "comment created", + "data": serializer.data} + return Response(response, status=status.HTTP_201_CREATED) + return Response("Not allowed; " + f"{user} isn't contributor for project {project}", + status=status.HTTP_403_FORBIDDEN) \ No newline at end of file