From 24ea4c0a521b2253f2d86f07def25e92240150c1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 20 Mar 2020 15:58:14 +0100 Subject: [PATCH] Comment code --- apps/permission/apps.py | 1 - apps/permission/backends.py | 13 +++++++++++ apps/permission/models.py | 32 +++++++++++++++++++++++++-- apps/permission/permissions.py | 5 +++++ apps/permission/templatetags/perms.py | 6 +++++ apps/permission/tests.py | 6 ----- apps/permission/views.py | 6 ----- note_kfet/settings/base.py | 9 +++----- 8 files changed, 57 insertions(+), 21 deletions(-) delete mode 100644 apps/permission/tests.py delete mode 100644 apps/permission/views.py diff --git a/apps/permission/apps.py b/apps/permission/apps.py index c0caa41b..2287fec4 100644 --- a/apps/permission/apps.py +++ b/apps/permission/apps.py @@ -9,7 +9,6 @@ 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/backends.py b/apps/permission/backends.py index a3b49ae0..3d911b1a 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -13,18 +13,31 @@ from member.models import Membership, Club class PermissionBackend(ModelBackend): + """ + Manage permissions of users + """ supports_object_permissions = True supports_anonymous_user = False supports_inactive_user = False @staticmethod def permissions(user, model, type): + """ + List all permissions of the given user that applies to a given model and a give type + :param user: The owner of the permissions + :param model: The model that the permissions shoud apply + :param type: The type of the permissions: view, change, add or delete + :return: A generator of the requested permissions + """ for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ .filter( rolepermissions__role__membership__user=user, model__app_label=model.app_label, # For polymorphic models, we don't filter on model type type=type, ).all(): + if not isinstance(model, permission.model.__class__): + continue + club = Club.objects.get(pk=permission.club) permission = permission.about( user=user, diff --git a/apps/permission/models.py b/apps/permission/models.py index 1c076918..109c1875 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -57,6 +57,10 @@ class InstancedPermission: return False def update_query(self): + """ + The query is not analysed in a first time. It is analysed at most once if needed. + :return: + """ if not self.query: # noinspection PyProtectedMember self.query = Permission._about(self.raw_query, **self.kwargs) @@ -72,6 +76,10 @@ class InstancedPermission: class PermissionMask(models.Model): + """ + Permissions that are hidden behind a mask + """ + rank = models.PositiveSmallIntegerField( unique=True, verbose_name=_('rank'), @@ -106,9 +114,9 @@ class Permission(models.Model): # query -> {key: value, …} A list of fields and values of a Q object # key -> string A field name # value -> int | string | bool | null Literal values - # | [parameter] A parameter + # | [parameter, …] A parameter. See compute_param for more details. # | {"F": oper} An F object - # oper -> [string] A parameter + # oper -> [string, …] A parameter. See compute_param for more details. # | ["ADD", oper, …] Sum multiple F objects or literal # | ["SUB", oper, oper] Substract two F objects or literal # | ["MUL", oper, …] Multiply F objects or literals @@ -164,6 +172,18 @@ class Permission(models.Model): @staticmethod def compute_param(value, **kwargs): + """ + A parameter is given by a list. The first argument is the name of the parameter. + The parameters are the user, the club, and some classes (Note, ...) + If there are more arguments in the list, then attributes are queried. + For example, ["user", "note", "balance"] will return the balance of the note of the user. + If an argument is a list, then this is interpreted with a function call: + First argument is the name of the function, next arguments are parameters, and if there is a dict, + then the dict is given as kwargs. + For example: NoteUser.objects.filter(user__memberships__club__name="Kfet").all() is translated by: + ["NoteUser", "objects", ["filter", {"user__memberships__club__name": "Kfet"}], ["all"]] + """ + if not isinstance(value, list): return value @@ -192,6 +212,12 @@ class Permission(models.Model): @staticmethod def _about(query, **kwargs): + """ + Translate JSON query into a Q query. + :param query: The JSON query + :param kwargs: Additional params + :return: A Q object + """ if len(query) == 0: # The query is either [] or {} and # applies to all objects of the model @@ -204,6 +230,8 @@ class Permission(models.Model): return functools.reduce(operator.or_, [Permission._about(query, **kwargs) for query in query[1:]]) elif query[0] == 'NOT': return ~Permission._about(query[1], **kwargs) + else: + return Q(pk=F("pk")) elif isinstance(query, dict): q_kwargs = {} for key in query: diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index 1cbae474..d9816a63 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -7,6 +7,11 @@ SAFE_METHODS = ('HEAD', 'OPTIONS', ) class StrongDjangoObjectPermissions(DjangoObjectPermissions): + """ + Default DjangoObjectPermissions grant view permission to all. + This is a simple patch of this class that controls view access. + """ + perms_map = { 'GET': ['%(app_label)s.view_%(model_name)s'], 'OPTIONS': [], diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 33cd46a6..8f2a0006 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -12,6 +12,9 @@ from permission.backends import PermissionBackend @stringfilter def not_empty_model_list(model_name): + """ + Return True if and only if the current user has right to see any object of the given model. + """ user = get_current_authenticated_user() session = get_current_session() if user is None: @@ -29,6 +32,9 @@ def not_empty_model_list(model_name): @stringfilter def not_empty_model_change_list(model_name): + """ + Return True if and only if the current user has right to change any object of the given model. + """ user = get_current_authenticated_user() session = get_current_session() if user is None: diff --git a/apps/permission/tests.py b/apps/permission/tests.py deleted file mode 100644 index b5d5752e..00000000 --- a/apps/permission/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.test import TestCase - -# Create your tests here. diff --git a/apps/permission/views.py b/apps/permission/views.py deleted file mode 100644 index 8d81fd33..00000000 --- a/apps/permission/views.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.shortcuts import render - -# Create your views here. diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index e56555bd..bf983f3b 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -127,17 +127,14 @@ PASSWORD_HASHERS = [ 'member.hashers.CustomNK15Hasher', ] -# Django Guardian object permissions - AUTHENTICATION_BACKENDS = ( - 'permission.backends.PermissionBackend', - 'cas.backends.CASBackend', + 'permission.backends.PermissionBackend', # Custom role-based permission system + 'cas.backends.CASBackend', # For CAS connections ) REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ + # Control API access with our role-based permission system 'permission.permissions.StrongDjangoObjectPermissions', ], 'DEFAULT_AUTHENTICATION_CLASSES': [