From 057f42fdb67195d5cd434924bb083074eaf35a99 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <yohann.danello@gmail.com>
Date: Wed, 18 Mar 2020 14:42:35 +0100
Subject: [PATCH] Handle permissions (and it seems working!)

---
 apps/activity/api/views.py         |  9 ++--
 apps/api/urls.py                   | 12 +++--
 apps/api/viewsets.py               | 26 +++++++++++
 apps/logs/api/views.py             |  4 +-
 apps/member/api/views.py           | 10 ++--
 apps/member/backends.py            | 57 ++++++++++++++++++++---
 apps/member/views.py               |  1 -
 apps/note/api/serializers.py       |  6 +++
 apps/note/api/views.py             | 24 +++++-----
 apps/permission/__init__.py        |  4 ++
 apps/permission/api/__init__.py    |  0
 apps/permission/api/serializers.py | 17 +++++++
 apps/permission/api/urls.py        | 11 +++++
 apps/permission/api/views.py       | 20 ++++++++
 apps/permission/apps.py            |  7 +++
 apps/permission/models.py          | 70 +++++++++++++++++++---------
 apps/permission/permissions.py     | 58 +++++++++++++++++++++++
 apps/permission/signals.py         | 75 ++++++++++++++++++++++++++++++
 note_kfet/settings/base.py         |  3 +-
 19 files changed, 357 insertions(+), 57 deletions(-)
 create mode 100644 apps/api/viewsets.py
 create mode 100644 apps/permission/api/__init__.py
 create mode 100644 apps/permission/api/serializers.py
 create mode 100644 apps/permission/api/urls.py
 create mode 100644 apps/permission/api/views.py
 create mode 100644 apps/permission/permissions.py
 create mode 100644 apps/permission/signals.py

diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py
index 4ee2194d..651560fd 100644
--- a/apps/activity/api/views.py
+++ b/apps/activity/api/views.py
@@ -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,
diff --git a/apps/api/urls.py b/apps/api/urls.py
index 95ed5f99..40e6c572 100644
--- a/apps/api/urls.py
+++ b/apps/api/urls.py
@@ -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'
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
new file mode 100644
index 00000000..cb32b09e
--- /dev/null
+++ b/apps/api/viewsets.py
@@ -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"))
diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py
index 2c47b7a2..6bd4f721 100644
--- a/apps/logs/api/views.py
+++ b/apps/logs/api/views.py
@@ -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,
diff --git a/apps/member/api/views.py b/apps/member/api/views.py
index c85df903..b4715cae 100644
--- a/apps/member/api/views.py
+++ b/apps/member/api/views.py
@@ -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,
diff --git a/apps/member/backends.py b/apps/member/backends.py
index db227cdb..3fdbd8d1 100644
--- a/apps/member/backends.py
+++ b/apps/member/backends.py
@@ -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
diff --git a/apps/member/views.py b/apps/member/views.py
index dacfde33..2213f37d 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -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):
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index 85f500ed..02311de1 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -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
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index 29c79bd8..6a3bb41e 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -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,
diff --git a/apps/permission/__init__.py b/apps/permission/__init__.py
index e69de29b..4e3eb6bc 100644
--- a/apps/permission/__init__.py
+++ b/apps/permission/__init__.py
@@ -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'
diff --git a/apps/permission/api/__init__.py b/apps/permission/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/permission/api/serializers.py b/apps/permission/api/serializers.py
new file mode 100644
index 00000000..0a52f4fe
--- /dev/null
+++ b/apps/permission/api/serializers.py
@@ -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__'
diff --git a/apps/permission/api/urls.py b/apps/permission/api/urls.py
new file mode 100644
index 00000000..d50344ea
--- /dev/null
+++ b/apps/permission/api/urls.py
@@ -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)
diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py
new file mode 100644
index 00000000..6087c83e
--- /dev/null
+++ b/apps/permission/api/views.py
@@ -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', ]
diff --git a/apps/permission/apps.py b/apps/permission/apps.py
index c9c912a5..c0caa41b 100644
--- a/apps/permission/apps.py
+++ b/apps/permission/apps.py
@@ -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)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 9584f59f..b90fcfb9 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -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
diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py
new file mode 100644
index 00000000..1cbae474
--- /dev/null
+++ b/apps/permission/permissions.py
@@ -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
diff --git a/apps/permission/signals.py b/apps/permission/signals.py
new file mode 100644
index 00000000..a051482e
--- /dev/null
+++ b/apps/permission/signals.py
@@ -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
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index 29ff49c5..800c798e 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -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',
-- 
GitLab