From 67d1d9f7b72f11a58e947909ccc198284afe2c43 Mon Sep 17 00:00:00 2001 From: Benjamin Graillot 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 = [