From 67d1d9f7b72f11a58e947909ccc198284afe2c43 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot <graillot@crans.org>
Date: Wed, 18 Sep 2019 14:26:42 +0200
Subject: [PATCH] Added permission app

---
 apps/member/backends.py     |  33 +++++++++++
 apps/member/models.py       |  22 +++++++
 apps/permission/__init__.py |   0
 apps/permission/admin.py    |   3 +
 apps/permission/apps.py     |   5 ++
 apps/permission/models.py   | 112 ++++++++++++++++++++++++++++++++++++
 apps/permission/tests.py    |   3 +
 apps/permission/views.py    |   3 +
 note_kfet/settings.py       |   1 +
 9 files changed, 182 insertions(+)
 create mode 100644 apps/member/backends.py
 create mode 100644 apps/permission/__init__.py
 create mode 100644 apps/permission/admin.py
 create mode 100644 apps/permission/apps.py
 create mode 100644 apps/permission/models.py
 create mode 100644 apps/permission/tests.py
 create mode 100644 apps/permission/views.py

diff --git a/apps/member/backends.py b/apps/member/backends.py
new file mode 100644
index 00000000..0b2edad8
--- /dev/null
+++ b/apps/member/backends.py
@@ -0,0 +1,33 @@
+from django.contribs.contenttype.models import ContentType
+from member.models import Club, Membership, RolePermissions
+
+
+class PermissionBackend(object):
+    supports_object_permissions = True
+    supports_anonymous_user = False
+    supports_inactive_user = False
+
+    def authenticate(self, username, password):
+        return None
+
+    def permissions(self, user, obj):
+        for membership in user.memberships.all():
+            if not membership.valid() or membership.role is None:
+                continue
+            for permission in RolePermissions.objects.get(role=membership.role).permissions.objects.all():
+                permission = permission.about(user=user, club=membership.club)
+                yield permission
+
+    def has_perm(self, user_obj, perm, obj=None):
+        if obj is None:
+            return False
+        perm = perm.split('_')
+        perm_type = perm[1]
+        perm_field = perm[2] if len(perm) == 3 else None
+        return any(permission.applies(obj, perm_type, perm_field) for obj in self.permissions(user_obj, obj))
+
+    def get_all_permissions(self, user_obj, obj=None):
+        if obj is None:
+            return []
+        else:
+            return list(self.permissions(user_obj, obj))
diff --git a/apps/member/models.py b/apps/member/models.py
index 70f8ccf7..7eacdc60 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -2,6 +2,8 @@
 # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+import datetime
+
 from django.conf import settings
 from django.db import models
 from django.db.models.signals import post_save
@@ -9,6 +11,7 @@ from django.dispatch import receiver
 from django.utils.translation import gettext_lazy as _
 from django.urls import reverse
 
+
 class Profile(models.Model):
     """
     An user profile
@@ -51,6 +54,7 @@ class Profile(models.Model):
     def get_absolute_url(self):
         return reverse('user_detail',args=(self.pk,))
 
+
 class Club(models.Model):
     """
     A student club
@@ -141,11 +145,29 @@ class Membership(models.Model):
         verbose_name=_('fee'),
     )
 
+    def valid(self):
+        return self.date_start <= datetime.datetime.now() < self.date_end
+
     class Meta:
         verbose_name = _('membership')
         verbose_name_plural = _('memberships')
 
 
+class RolePermissions(models.Model):
+    """
+    Permissions associated with a Role
+    """
+    role = models.ForeignKey(
+        Role,
+        on_delete=models.PROTECT,
+        related_name='+',
+        verbose_name=_('role'),
+    )
+    permissions = models.ManyToManyField(
+        'permission.Permission'
+    )
+
+
 # @receiver(post_save, sender=settings.AUTH_USER_MODEL)
 # def save_user_profile(instance, created, **_kwargs):
 #     """
diff --git a/apps/permission/__init__.py b/apps/permission/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/permission/admin.py b/apps/permission/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/apps/permission/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/apps/permission/apps.py b/apps/permission/apps.py
new file mode 100644
index 00000000..0f46ef08
--- /dev/null
+++ b/apps/permission/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class PermissionConfig(AppConfig):
+    name = 'permission'
diff --git a/apps/permission/models.py b/apps/permission/models.py
new file mode 100644
index 00000000..b7cc8845
--- /dev/null
+++ b/apps/permission/models.py
@@ -0,0 +1,112 @@
+import json
+
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.db.models import Q
+from django.utils.translation import gettext_lazy as _
+
+
+class InstancedPermission:
+
+    def __init__(self, model, permission, type, field):
+        self.model = model
+        self.permission = permission
+        self.type = type
+        self.field = field
+
+    def applies(self, obj, permission_type, field_name=None):
+        if ContentType.objects.get_for_model(obj) != self.model:
+            # The permission does not apply to the object
+            return False
+        if self.permission is None:
+            if permission_type == self.type:
+                if field_name is not None:
+                    return field_name == self.field
+                else:
+                    return True
+            else:
+                return False
+        elif isinstance(self.permission, dict):
+            for field in self.permission:
+                value = getattr(obj, field)
+                if isinstance(value, models.Model):
+                    value = value.pk
+                if value != self.permission[field]:
+                    return False
+        elif isinstance(self.permission, type(obj.pk)):
+            if obj.pk != self.permission:
+                return False
+        if permission_type == self.type:
+            if field_name:
+                return field_name == self.field
+            else:
+                return True
+        return False
+
+    def __repr__(self):
+        if self.field:
+            return _("Can {type} {model}.{field} in {permission}").format(type=self.type, model=self.model, field=self.field, permission=self.permission)
+        else:
+            return _("Can {type} {model} in {permission}").format(type=self.type, model=self.model, permission=self.permission)
+
+
+class Permission(models.Model):
+
+    PERMISSION_TYPES = [
+        ('C', 'add'),
+        ('R', 'view'),
+        ('U', 'change'),
+        ('D', 'delete')
+    ]
+
+    model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
+
+    permission = models.TextField()
+
+    type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
+
+    field = models.CharField(max_length=255, blank=True)
+
+    class Meta:
+        unique_together = ('model', 'permission', 'type', 'field')
+
+    def clean(self):
+        if self.field and self.type not in {'R', 'U'}:
+            raise ValidationError(_("Specifying field applies only to view and change permission types."))
+
+    def save(self):
+        self.full_clean()
+        super().save()
+
+    def _about(_self, _permission, **kwargs):
+        if _permission[0] == 'all':
+            return None
+        elif _permission[0] == 'pk':
+            if _permission[1] in kwargs:
+                return kwargs[_permission[1]].pk
+            else:
+                return None
+        elif _permission[0] == 'filter':
+            return {field: _self._about(_permission[1][field], **kwargs) for field in _permission[1]}
+        else:
+            return _permission
+
+    def about(self, **kwargs):
+        permission = json.loads(self.permission)
+        permission = self._about(permission, **kwargs)
+        return InstancedPermission(self.model, permission, self.type, self.field)
+
+    def __str__(self):
+        if self.field:
+            return _("Can {type} {model}.{field} in {permission}").format(type=self.type, model=self.model, field=self.field, permission=self.permission)
+        else:
+            return _("Can {type} {model} in {permission}").format(type=self.type, model=self.model, permission=self.permission)
+
+
+class UserPermission(models.Model):
+
+    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
+
+    permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
+
diff --git a/apps/permission/tests.py b/apps/permission/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/apps/permission/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/permission/views.py b/apps/permission/views.py
new file mode 100644
index 00000000..91ea44a2
--- /dev/null
+++ b/apps/permission/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/note_kfet/settings.py b/note_kfet/settings.py
index cfe09f7b..3cd3b717 100644
--- a/note_kfet/settings.py
+++ b/note_kfet/settings.py
@@ -56,6 +56,7 @@ INSTALLED_APPS = [
     'activity',
     'member',
     'note',
+    'permission'
 ]
 
 MIDDLEWARE = [
-- 
GitLab