Comment code

This commit is contained in:
Yohann D'ANELLO 2020-03-20 15:58:14 +01:00
parent 091c427707
commit 24ea4c0a52
8 changed files with 57 additions and 21 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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:

View File

@ -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': [],

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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': [