mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	Handle permissions (and it seems working!)
This commit is contained in:
		@@ -1,14 +1,15 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet
 | 
			
		||||
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
 | 
			
		||||
from ..models import ActivityType, Activity, Guest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityTypeViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -20,7 +21,7 @@ class ActivityTypeViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    filterset_fields = ['name', 'can_invite', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class ActivityViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -32,7 +33,7 @@ class ActivityViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    filterset_fields = ['name', 'description', 'activity_type', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GuestViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class GuestViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,16 @@ from django.conf.urls import url, include
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework import routers, serializers, viewsets
 | 
			
		||||
from rest_framework import routers, serializers
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
			
		||||
 | 
			
		||||
from activity.api.urls import register_activity_urls
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet
 | 
			
		||||
from member.api.urls import register_members_urls
 | 
			
		||||
from note.api.urls import register_note_urls
 | 
			
		||||
from logs.api.urls import register_logs_urls
 | 
			
		||||
from permission.api.urls import register_permission_urls
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -39,7 +43,7 @@ class ContentTypeSerializer(serializers.ModelSerializer):
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class UserViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -52,7 +56,8 @@ class UserViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    search_fields = ['$username', '$first_name', '$last_name', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
# This ViewSet is the only one that is accessible from all authenticated users!
 | 
			
		||||
class ContentTypeViewSet(ReadOnlyModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -70,6 +75,7 @@ router.register('user', UserViewSet)
 | 
			
		||||
register_members_urls(router, 'members')
 | 
			
		||||
register_activity_urls(router, 'activity')
 | 
			
		||||
register_note_urls(router, 'note')
 | 
			
		||||
register_permission_urls(router, 'permission')
 | 
			
		||||
register_logs_urls(router, 'logs')
 | 
			
		||||
 | 
			
		||||
app_name = 'api'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from member.backends import PermissionBackend
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    Protect a ModelViewSet by filtering the objects that the user cannot see.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
 | 
			
		||||
        return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        model = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
 | 
			
		||||
        return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view"))
 | 
			
		||||
@@ -2,14 +2,14 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.filters import OrderingFilter
 | 
			
		||||
 | 
			
		||||
from api.viewsets import ReadOnlyProtectedModelViewSet
 | 
			
		||||
from .serializers import ChangelogSerializer
 | 
			
		||||
from ..models import Changelog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangelogViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet
 | 
			
		||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
 | 
			
		||||
from ..models import Profile, Club, Role, Membership
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProfileViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class ProfileViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -18,7 +18,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = ProfileSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClubViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class ClubViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -30,7 +30,7 @@ class ClubViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    search_fields = ['$name', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class RoleViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -42,7 +42,7 @@ class RoleViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    search_fields = ['$name', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MembershipViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class MembershipViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,12 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from member.models import Club, Membership, RolePermissions
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
from django.db.models import Q, F
 | 
			
		||||
 | 
			
		||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
 | 
			
		||||
from .models import Membership, RolePermissions, Club
 | 
			
		||||
from django.contrib.auth.backends import ModelBackend
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -14,21 +19,61 @@ class PermissionBackend(ModelBackend):
 | 
			
		||||
        for membership in Membership.objects.filter(user=user).all():
 | 
			
		||||
            if not membership.valid() or membership.roles is None:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            for role_permissions in RolePermissions.objects.filter(role=membership.roles).all():
 | 
			
		||||
                for permission in role_permissions.permissions.all():
 | 
			
		||||
                    permission = permission.about(user=user, club=membership.club)
 | 
			
		||||
                    permission = permission.about(
 | 
			
		||||
                        user=user,
 | 
			
		||||
                        club=membership.club,
 | 
			
		||||
                        User=User,
 | 
			
		||||
                        Club=Club,
 | 
			
		||||
                        Membership=Membership,
 | 
			
		||||
                        Note=Note,
 | 
			
		||||
                        NoteUser=NoteUser,
 | 
			
		||||
                        NoteClub=NoteClub,
 | 
			
		||||
                        NoteSpecial=NoteSpecial,
 | 
			
		||||
                        F=F,
 | 
			
		||||
                        Q=Q
 | 
			
		||||
                    )
 | 
			
		||||
                    yield permission
 | 
			
		||||
 | 
			
		||||
    def filter_queryset(self, user, model, type, field=None):
 | 
			
		||||
        """
 | 
			
		||||
        Filter a queryset by considering the permissions of a given user.
 | 
			
		||||
        :param user: The owner of the permissions that are fetched
 | 
			
		||||
        :param model: The concerned model of the queryset
 | 
			
		||||
        :param type: The type of modification (view, add, change, delete)
 | 
			
		||||
        :param field: The field of the model to test, if concerned
 | 
			
		||||
        :return: A query that corresponds to the filter to give to a queryset
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if user.is_superuser:
 | 
			
		||||
            # Superusers have all rights
 | 
			
		||||
            return Q()
 | 
			
		||||
 | 
			
		||||
        # Never satisfied
 | 
			
		||||
        query = Q(pk=-1)
 | 
			
		||||
        for perm in self.permissions(user):
 | 
			
		||||
            if field and field != perm.field:
 | 
			
		||||
                continue
 | 
			
		||||
            if perm.model != model or perm.type != type:
 | 
			
		||||
                continue
 | 
			
		||||
            query = query | perm.query
 | 
			
		||||
        return query
 | 
			
		||||
 | 
			
		||||
    def has_perm(self, user_obj, perm, obj=None):
 | 
			
		||||
        if user_obj.is_superuser:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        if obj is None:
 | 
			
		||||
            return False
 | 
			
		||||
        perm = perm.split('_', 3)
 | 
			
		||||
        perm_type = perm[1]
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        perm = perm.split('.')[-1].split('_', 2)
 | 
			
		||||
        perm_type = perm[0]
 | 
			
		||||
        perm_field = perm[2] if len(perm) == 3 else None
 | 
			
		||||
        return any(permission.applies(obj, perm_type, perm_field) for permission in self.permissions(user_obj))
 | 
			
		||||
        if any(permission.applies(obj, perm_type, perm_field) for permission in self.permissions(user_obj)):
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def has_module_perms(self, user_obj, app_label):
 | 
			
		||||
        return False
 | 
			
		||||
 
 | 
			
		||||
@@ -203,7 +203,6 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView):
 | 
			
		||||
        return HttpResponseRedirect(self.get_success_url())
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        print(self.request)
 | 
			
		||||
        return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk})
 | 
			
		||||
 | 
			
		||||
    def get(self, request, *args, **kwargs):
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,9 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
			
		||||
        NoteSpecial: NoteSpecialSerializer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Note
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateCategorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
@@ -162,3 +165,6 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
 | 
			
		||||
        MembershipTransaction: MembershipTransactionSerializer,
 | 
			
		||||
        SpecialTransaction: SpecialTransactionSerializer,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Transaction
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
			
		||||
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet
 | 
			
		||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
 | 
			
		||||
    NoteUserSerializer, AliasSerializer, \
 | 
			
		||||
    TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
 | 
			
		||||
@@ -13,7 +13,7 @@ from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
			
		||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NoteViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -23,7 +23,7 @@ class NoteViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = NoteSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteClubViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NoteClubViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -33,7 +33,7 @@ class NoteClubViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = NoteClubSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NoteSpecialViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -43,7 +43,7 @@ class NoteSpecialViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = NoteSpecialSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteUserViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NoteUserViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -53,7 +53,7 @@ class NoteUserViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = NoteUserSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -70,7 +70,7 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
			
		||||
        Parse query and apply filters.
 | 
			
		||||
        :return: The filtered set of requested notes
 | 
			
		||||
        """
 | 
			
		||||
        queryset = Note.objects.all()
 | 
			
		||||
        queryset = super().get_queryset()
 | 
			
		||||
 | 
			
		||||
        alias = self.request.query_params.get("alias", ".*")
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
@@ -92,7 +92,7 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
			
		||||
        return queryset.distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class AliasViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -110,7 +110,7 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
        :return: The filtered set of requested aliases
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        queryset = Alias.objects.all()
 | 
			
		||||
        queryset = super().get_queryset()
 | 
			
		||||
 | 
			
		||||
        alias = self.request.query_params.get("alias", ".*")
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
@@ -138,7 +138,7 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateCategoryViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -150,7 +150,7 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    search_fields = ['$name', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TransactionTemplateViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -162,7 +162,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    filterset_fields = ['name', 'amount', 'display', 'category', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TransactionViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
default_app_config = 'permission.apps.PermissionConfig'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from ..models import Permission
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PermissionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Permission types.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `Permission` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Permission
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
							
								
								
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from .views import PermissionViewSet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_permission_urls(router, path):
 | 
			
		||||
    """
 | 
			
		||||
    Configure router for permission REST API.
 | 
			
		||||
    """
 | 
			
		||||
    router.register(path, PermissionViewSet)
 | 
			
		||||
							
								
								
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
 | 
			
		||||
from api.viewsets import ReadOnlyProtectedModelViewSet
 | 
			
		||||
from .serializers import PermissionSerializer
 | 
			
		||||
from ..models import Permission
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/logs/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Permission.objects.all()
 | 
			
		||||
    serializer_class = PermissionSerializer
 | 
			
		||||
    filter_backends = [DjangoFilterBackend]
 | 
			
		||||
    filterset_fields = ['model', 'type', ]
 | 
			
		||||
@@ -2,7 +2,14 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
from django.db.models.signals import pre_save, pre_delete
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PermissionConfig(AppConfig):
 | 
			
		||||
    name = 'permission'
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        # noinspection PyUnresolvedReferences
 | 
			
		||||
        from . import signals
 | 
			
		||||
        pre_save.connect(signals.pre_save_object)
 | 
			
		||||
        pre_delete.connect(signals.pre_delete_object)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,13 @@ class InstancedPermission:
 | 
			
		||||
        """
 | 
			
		||||
        if self.type == 'add':
 | 
			
		||||
            if permission_type == self.type:
 | 
			
		||||
                return obj in self.model.modelclass().objects.get(self.query)
 | 
			
		||||
                return self.query(obj)
 | 
			
		||||
 | 
			
		||||
        if ContentType.objects.get_for_model(obj) != self.model:
 | 
			
		||||
            # The permission does not apply to the model
 | 
			
		||||
            return False
 | 
			
		||||
        if permission_type == self.type:
 | 
			
		||||
            if field_name and field_name != self.field:
 | 
			
		||||
            if self.field and field_name != self.field:
 | 
			
		||||
                return False
 | 
			
		||||
            return obj in self.model.model_class().objects.filter(self.query).all()
 | 
			
		||||
        else:
 | 
			
		||||
@@ -91,6 +92,7 @@ class Permission(models.Model):
 | 
			
		||||
        unique_together = ('model', 'query', 'type', 'field')
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        self.query = json.dumps(json.loads(self.query))
 | 
			
		||||
        if self.field and self.type not in {'view', 'change'}:
 | 
			
		||||
            raise ValidationError(_("Specifying field applies only to view and change permission types."))
 | 
			
		||||
 | 
			
		||||
@@ -101,21 +103,45 @@ class Permission(models.Model):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compute_f(oper, **kwargs):
 | 
			
		||||
        if isinstance(oper, list):
 | 
			
		||||
            if len(oper) == 1:
 | 
			
		||||
                return kwargs[oper[0]].pk
 | 
			
		||||
            elif len(oper) >= 2:
 | 
			
		||||
                if oper[0] == 'ADD':
 | 
			
		||||
                    return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
			
		||||
                elif oper[0] == 'SUB':
 | 
			
		||||
                    return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
 | 
			
		||||
                elif oper[0] == 'MUL':
 | 
			
		||||
                    return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
			
		||||
                elif oper[0] == 'F':
 | 
			
		||||
                    return F(oper[1])
 | 
			
		||||
            if oper[0] == 'ADD':
 | 
			
		||||
                return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
			
		||||
            elif oper[0] == 'SUB':
 | 
			
		||||
                return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
 | 
			
		||||
            elif oper[0] == 'MUL':
 | 
			
		||||
                return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
			
		||||
            elif oper[0] == 'F':
 | 
			
		||||
                return F(oper[1])
 | 
			
		||||
            else:
 | 
			
		||||
                field = kwargs[oper[0]]
 | 
			
		||||
                for i in range(1, len(oper)):
 | 
			
		||||
                    field = getattr(field, oper[i])
 | 
			
		||||
                return field
 | 
			
		||||
        else:
 | 
			
		||||
            return oper
 | 
			
		||||
        # TODO: find a better way to crash here
 | 
			
		||||
        raise Exception("F is wrong")
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compute_param(value, **kwargs):
 | 
			
		||||
        if not isinstance(value, list):
 | 
			
		||||
            return value
 | 
			
		||||
 | 
			
		||||
        field = kwargs[value[0]]
 | 
			
		||||
        for i in range(1, len(value)):
 | 
			
		||||
            if isinstance(value[i], list):
 | 
			
		||||
                field = getattr(field, value[i][0])
 | 
			
		||||
                params = []
 | 
			
		||||
                call_kwargs = {}
 | 
			
		||||
                for j in range(1, len(value[i])):
 | 
			
		||||
                    param = Permission.compute_param(value[i][j], **kwargs)
 | 
			
		||||
                    if isinstance(param, dict):
 | 
			
		||||
                        for key in param:
 | 
			
		||||
                            val = Permission.compute_param(param[key], **kwargs)
 | 
			
		||||
                            call_kwargs[key] = val
 | 
			
		||||
                    else:
 | 
			
		||||
                        params.append(param)
 | 
			
		||||
                field = field(*params, **call_kwargs)
 | 
			
		||||
            else:
 | 
			
		||||
                field = getattr(field, value[i])
 | 
			
		||||
        return field
 | 
			
		||||
 | 
			
		||||
    def _about(self, query, **kwargs):
 | 
			
		||||
        if self.type == 'add':
 | 
			
		||||
@@ -124,8 +150,8 @@ class Permission(models.Model):
 | 
			
		||||
        if len(query) == 0:
 | 
			
		||||
            # The query is either [] or {} and
 | 
			
		||||
            # applies to all objects of the model
 | 
			
		||||
            # to represent this we return None
 | 
			
		||||
            return None
 | 
			
		||||
            # to represent this we return a trivial request
 | 
			
		||||
            return Q(pk=F("pk"))
 | 
			
		||||
        if isinstance(query, list):
 | 
			
		||||
            if query[0] == 'AND':
 | 
			
		||||
                return functools.reduce(operator.and_, [self._about(query, **kwargs) for query in query[1:]])
 | 
			
		||||
@@ -138,11 +164,11 @@ class Permission(models.Model):
 | 
			
		||||
            for key in query:
 | 
			
		||||
                value = query[key]
 | 
			
		||||
                if isinstance(value, list):
 | 
			
		||||
                    # It is a parameter we query its primary key
 | 
			
		||||
                    q_kwargs[key] = kwargs[value[0]].pk
 | 
			
		||||
                    # It is a parameter we query its return value
 | 
			
		||||
                    q_kwargs[key] = Permission.compute_param(value, **kwargs)
 | 
			
		||||
                elif isinstance(value, dict):
 | 
			
		||||
                    # It is an F object
 | 
			
		||||
                    q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
 | 
			
		||||
                    q_kwargs[key] = Permission.compute_f(value['F'], **kwargs)
 | 
			
		||||
                else:
 | 
			
		||||
                    q_kwargs[key] = value
 | 
			
		||||
            return Q(**q_kwargs)
 | 
			
		||||
@@ -167,7 +193,7 @@ class Permission(models.Model):
 | 
			
		||||
                value = query[key]
 | 
			
		||||
                if isinstance(value, list):
 | 
			
		||||
                    # It is a parameter we query its primary key
 | 
			
		||||
                    q_kwargs[key] = kwargs[value[0]].pk
 | 
			
		||||
                    q_kwargs[key] = Permission.compute_param(value, **kwargs)
 | 
			
		||||
                elif isinstance(value, dict):
 | 
			
		||||
                    # It is an F object
 | 
			
		||||
                    q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
 | 
			
		||||
@@ -176,7 +202,7 @@ class Permission(models.Model):
 | 
			
		||||
            def func(obj):
 | 
			
		||||
                nonlocal q_kwargs
 | 
			
		||||
                for arg in q_kwargs:
 | 
			
		||||
                    if getattr(obj, arg) != q_kwargs(arg):
 | 
			
		||||
                    if getattr(obj, arg) != q_kwargs[arg]:
 | 
			
		||||
                        return False
 | 
			
		||||
                return True
 | 
			
		||||
            return func
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from rest_framework.permissions import DjangoObjectPermissions
 | 
			
		||||
 | 
			
		||||
SAFE_METHODS = ('HEAD', 'OPTIONS', )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StrongDjangoObjectPermissions(DjangoObjectPermissions):
 | 
			
		||||
    perms_map = {
 | 
			
		||||
        'GET': ['%(app_label)s.view_%(model_name)s'],
 | 
			
		||||
        'OPTIONS': [],
 | 
			
		||||
        'HEAD': [],
 | 
			
		||||
        '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_required_object_permissions(self, method, model_cls):
 | 
			
		||||
        kwargs = {
 | 
			
		||||
            'app_label': model_cls._meta.app_label,
 | 
			
		||||
            'model_name': model_cls._meta.model_name
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if method not in self.perms_map:
 | 
			
		||||
            from rest_framework import exceptions
 | 
			
		||||
            raise exceptions.MethodNotAllowed(method)
 | 
			
		||||
 | 
			
		||||
        return [perm % kwargs for perm in self.perms_map[method]]
 | 
			
		||||
 | 
			
		||||
    def has_object_permission(self, request, view, obj):
 | 
			
		||||
        # authentication checks have already executed via has_permission
 | 
			
		||||
        queryset = self._queryset(view)
 | 
			
		||||
        model_cls = queryset.model
 | 
			
		||||
        user = request.user
 | 
			
		||||
 | 
			
		||||
        perms = self.get_required_object_permissions(request.method, model_cls)
 | 
			
		||||
 | 
			
		||||
        if not user.has_perms(perms, obj):
 | 
			
		||||
            # If the user does not have permissions we need to determine if
 | 
			
		||||
            # they have read permissions to see 403, or not, and simply see
 | 
			
		||||
            # a 404 response.
 | 
			
		||||
            from django.http import Http404
 | 
			
		||||
 | 
			
		||||
            if request.method in SAFE_METHODS:
 | 
			
		||||
                # Read permissions already checked and failed, no need
 | 
			
		||||
                # to make another lookup.
 | 
			
		||||
                raise Http404
 | 
			
		||||
 | 
			
		||||
            read_perms = self.get_required_object_permissions('GET', model_cls)
 | 
			
		||||
            if not user.has_perms(read_perms, obj):
 | 
			
		||||
                raise Http404
 | 
			
		||||
 | 
			
		||||
            # Has read permissions.
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
							
								
								
									
										75
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
from logs.middlewares import get_current_authenticated_user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
EXCLUDED = [
 | 
			
		||||
    'cas_server.proxygrantingticket',
 | 
			
		||||
    'cas_server.proxyticket',
 | 
			
		||||
    'cas_server.serviceticket',
 | 
			
		||||
    'cas_server.user',
 | 
			
		||||
    'cas_server.userattributes',
 | 
			
		||||
    'contenttypes.contenttype',
 | 
			
		||||
    'logs.changelog',
 | 
			
		||||
    'migrations.migration',
 | 
			
		||||
    'sessions.session',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pre_save_object(sender, instance, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Before a model get saved, we check the permissions
 | 
			
		||||
    """
 | 
			
		||||
    # noinspection PyProtectedMember
 | 
			
		||||
    if instance._meta.label_lower in EXCLUDED:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    user = get_current_authenticated_user()
 | 
			
		||||
    if user is None:
 | 
			
		||||
        # Action performed on shell is always granted
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    qs = sender.objects.filter(pk=instance.pk).all()
 | 
			
		||||
    model_name_full = instance._meta.label_lower.split(".")
 | 
			
		||||
    app_label = model_name_full[0]
 | 
			
		||||
    model_name = model_name_full[1]
 | 
			
		||||
 | 
			
		||||
    if qs.exists():
 | 
			
		||||
        if user.has_perm(app_label + ".change_" + model_name, instance):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        previous = qs.get()
 | 
			
		||||
        for field in instance._meta.fields:
 | 
			
		||||
            field_name = field.name
 | 
			
		||||
            old_value = getattr(previous, field.name)
 | 
			
		||||
            new_value = getattr(instance, field.name)
 | 
			
		||||
            if old_value == new_value:
 | 
			
		||||
                continue
 | 
			
		||||
            if not user.has_perm(app_label + ".change_" + model_name + "_" + field_name, instance):
 | 
			
		||||
                raise PermissionDenied
 | 
			
		||||
    else:
 | 
			
		||||
        if not user.has_perm(app_label + ".add_" + model_name, instance):
 | 
			
		||||
            raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pre_delete_object(sender, instance, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Before a model get deleted, we check the permissions
 | 
			
		||||
    """
 | 
			
		||||
    # noinspection PyProtectedMember
 | 
			
		||||
    if instance._meta.label_lower in EXCLUDED:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    user = get_current_authenticated_user()
 | 
			
		||||
    if user is None:
 | 
			
		||||
        # Action performed on shell is always granted
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    model_name_full = instance._meta.label_lower.split(".")
 | 
			
		||||
    app_label = model_name_full[0]
 | 
			
		||||
    model_name = model_name_full[1]
 | 
			
		||||
 | 
			
		||||
    if not user.has_perm(app_label + ".delete_" + model_name, instance):
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
@@ -139,8 +139,7 @@ REST_FRAMEWORK = {
 | 
			
		||||
    # Use Django's standard `django.contrib.auth` permissions,
 | 
			
		||||
    # or allow read-only access for unauthenticated users.
 | 
			
		||||
    'DEFAULT_PERMISSION_CLASSES': [
 | 
			
		||||
        # TODO Maybe replace it with our custom permissions system
 | 
			
		||||
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
 | 
			
		||||
        'permission.permissions.StrongDjangoObjectPermissions',
 | 
			
		||||
    ],
 | 
			
		||||
    'DEFAULT_AUTHENTICATION_CLASSES': [
 | 
			
		||||
        'rest_framework.authentication.SessionAuthentication',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user