refactor permissions

This commit is contained in:
yann 2025-06-06 08:28:45 +02:00
parent b94c058598
commit c059b88101
6 changed files with 205 additions and 56 deletions

View File

@ -15,6 +15,13 @@ class UserSerializer(ModelSerializer):
'can_data_be_shared'] 'can_data_be_shared']
class UserListSerializer(ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']
class UserUpdateSerializer(ModelSerializer): class UserUpdateSerializer(ModelSerializer):
class Meta: class Meta:

View File

@ -7,7 +7,7 @@ class AdminProject(admin.ModelAdmin):
@admin.display(description='contributors') @admin.display(description='contributors')
def contributors(self, obj): def contributors(self, obj):
return obj.contributor return obj.contributors
class AdminIssue(admin.ModelAdmin): class AdminIssue(admin.ModelAdmin):
list_display = ('id', 'title', 'author', 'project') list_display = ('id', 'title', 'author', 'project')

View File

@ -39,9 +39,13 @@ class ProjectContributor(models.Model):
class Meta: class Meta:
unique_together = ('contributor', 'project') unique_together = ('contributor', 'project')
def get_user(self):
return self.contributor
def __str__(self): def __str__(self):
return self.contributor.username return self.contributor.username
class Issue(models.Model): class Issue(models.Model):
class Priority(models.TextChoices): class Priority(models.TextChoices):

View File

@ -1,5 +1,6 @@
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from support.models import Project from support.models import Project, Issue, Comment
class IsAuthor(BasePermission): class IsAuthor(BasePermission):
@ -13,7 +14,8 @@ class IsAuthor(BasePermission):
class IsContributor(BasePermission): class IsContributor(BasePermission):
def has_object_permission(self, request, view, object): def has_object_permission(self, request, view, object):
return bool(request.user print(object.contributors.all())
and request.user.is_authenticated return bool(request.user.is_authenticated
and request.user in object.contributors.all() and request.user in object.contributors.all()
) )

View File

@ -15,12 +15,15 @@ class ContributorSerializer(ModelSerializer):
class ContributorListSerializer(ModelSerializer): class ContributorListSerializer(ModelSerializer):
contributor = StringRelatedField(many=False)
class Meta: class Meta:
model = ProjectContributor model = ProjectContributor
fields = ['contributor'] fields = ['contributor']
class ProjectSerializer(ModelSerializer): class ProjectSerializer(ModelSerializer):
author = StringRelatedField(many=False) author = StringRelatedField(many=False)
@ -65,13 +68,9 @@ class IssueSerializer(ModelSerializer):
model = Issue model = Issue
fields = ['id', 'title', 'project', 'date_created', 'priority', fields = ['id', 'title', 'project', 'date_created', 'priority',
'tag', 'status', 'author'] '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): def validate_project(self, data):
# if data['user'] not in data['project'].contributors: # if data['user'] not in data['project'].contributors:
@ -84,15 +83,34 @@ class IssueSerializer(ModelSerializer):
return data 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): class CommentListSerializer(ModelSerializer):
issue = IssueSerializer(many=False) issue = IssueSerializer(many=False)
class Meta: class Meta:
model = Comment model = Comment
fields = ['title', 'date_created', 'author', 'issue'] fields = ['title', 'date_created', 'author', 'issue']
class CommentDetailSerializer(ModelSerializer): class CommentDetailSerializer(ModelSerializer):
class Meta: class Meta:
model = Comment model = Comment
fields = ['title', 'date_created', 'author', 'description'] fields = ['title', 'date_created', 'description', 'issue', 'author']

View File

@ -1,14 +1,19 @@
from django.contrib.auth.checks import check_models_permissions
from django.shortcuts import render from django.shortcuts import render
from rest_framework.serializers import raise_errors_on_nested_writes from rest_framework.serializers import raise_errors_on_nested_writes
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework.views import APIView
from support.models import Project, ProjectContributor, Issue, Comment from support.models import Project, ProjectContributor, Issue, Comment
from authentication.models import User from authentication.models import User
from support.serializers import (ProjectSerializer, from support.serializers import (ProjectSerializer,
ProjectDetailSerializer, ProjectDetailSerializer,
ContributorSerializer, ContributorSerializer,
ContributorListSerializer,
IssueSerializer, IssueSerializer,
IssueDetailSerializer,
CommentListSerializer, CommentListSerializer,
CommentDetailSerializer) CommentDetailSerializer)
from authentication.serializers import UserListSerializer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rest_framework.permissions import (IsAuthenticated, from rest_framework.permissions import (IsAuthenticated,
@ -16,6 +21,7 @@ from rest_framework.permissions import (IsAuthenticated,
from support.permissions import IsAuthor, IsContributor from support.permissions import IsAuthor, IsContributor
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from django.core.exceptions import PermissionDenied
class ProjectViewSet(ModelViewSet): class ProjectViewSet(ModelViewSet):
@ -26,10 +32,30 @@ class ProjectViewSet(ModelViewSet):
queryset = Project.objects.filter(active=True) queryset = Project.objects.filter(active=True)
def get_serializer_class(self):
if self.action == 'retrieve': def retrieve(self, request, pk, *args, **kwargs):
return self.detail_serializer_class """
return super().get_serializer_class() 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): def perform_create(self, serializer):
"""set authenticated user as author and contributor on creation""" """set authenticated user as author and contributor on creation"""
@ -39,65 +65,107 @@ class ProjectViewSet(ModelViewSet):
if contributor_serializer.is_valid(): if contributor_serializer.is_valid():
contributor_serializer.save() contributor_serializer.save()
@action(detail=True, methods=['patch'],
permission_classes=[IsContributor], @action(detail=True, methods=['get'], permission_classes=[IsContributor])
basename='add_contributor') def test(self, request, pk):
def add_contributor(self, request, pk): """only for testing purpose, should be deleted not published"""
"""Create the user/project contributor's relation""" if not request.user in Project.objects.get(id=pk).contributors.all():
if 'contributor' in request.data: raise PermissionDenied()
contributor = User.objects.get(username=request.data['contributor']) return Response("OK")
data = {'contributor': contributor.id, 'project': pk}
serializer = ContributorSerializer(data=data) @action(detail=True, methods=['patch'], permission_classes=[IsContributor])
if serializer.is_valid(): def contributor(self, request, pk):
serializer.save() """Add a contributor to a project
return Response(f"User {contributor} added", by creating a ProjectContributor's instance
status=status.HTTP_202_ACCEPTED) """
return Response("This user is already contributing", #check if requestor is contributor
status=status.HTTP_226_IM_USED) if not request.user in Project.objects.get(id=pk).contributors.all():
return Response(f"Key error;`contributor` is expected, " raise PermissionDenied()
f"not `{list(request.data)[0]}`",
status=status.HTTP_400_BAD_REQUEST) 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): class IssueViewSet(ModelViewSet):
permission_classes = [IsContributor] permission_classes = [IsAuthenticatedOrReadOnly]
serializer_class = IssueSerializer 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): 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'): if self.request.GET.get('project'):
project_id = int(self.request.GET.get('project')) project_id = int(self.request.GET.get('project'))
project = Project.objects.get(id=project_id) if not self.request.user in Project.objects.get(
self.check_object_permissions(self.request, project) id=project_id).contributors.all():
raise PermissionDenied()
return Issue.objects.filter(project=project_id) 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): def perform_update(self, serializer):
queryset = ProjectContributor.objects.filter(project=project) if self.request.user == serializer.author:
contributors_serializer = ContributorSerializer(queryset, many=True) return Response("OK")
return contributors_serializer.data 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): 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']) project = Project.objects.get(id=request.data['project'])
serializer = IssueSerializer(data=request.data) serializer = IssueSerializer(data=request.data)
if self.request.user not in project.contributors.all():
return Response("Requestor isn't contributor for this project",
print(request.data['project'], type(request.data['project'])) status=status.HTTP_403_FORBIDDEN)
print(self.get_contributors(request.data['project'])) if serializer.is_valid(raise_exception=True):
serializer.save(author=self.request.user)
if self.request.user in project.contributors: response = {
if serializer.is_valid(raise_exception=True): "message": f"Issue created for project {project}",
serializer.author = self.request.user "data": serializer.data
serializer.save() }
response = { return Response(response, status = status.HTTP_201_CREATED)
"message": f"Issue created for project {project}",
"data": serializer.data
}
return Response(response, status = status.HTTP_201_CREATED)
#def perform_create(self, serializer): #def perform_create(self, serializer):
# """set authenticated user as author and contributor on creation""" # """set authenticated user as author and contributor on creation"""
@ -105,17 +173,67 @@ class IssueViewSet(ModelViewSet):
class ContributorViewSet(ModelViewSet): class ContributorViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
serializer_class = ContributorSerializer serializer_class = ContributorSerializer
queryset = ProjectContributor.objects.all() 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): class CommentViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
serializer_class = CommentListSerializer serializer_class = CommentListSerializer
detail_serializer_class = CommentDetailSerializer detail_serializer_class = CommentDetailSerializer
queryset = Comment.objects.all() 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): def get_serializer_class(self):
if self.action == 'retrieve': if self.action == 'retrieve':
return self.detail_serializer_class return self.detail_serializer_class
return super().get_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)