From 67d1d9f7b72f11a58e947909ccc198284afe2c43 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Wed, 18 Sep 2019 14:26:42 +0200
Subject: [PATCH 001/119] 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 = [
From 2a4ab0975353a3c9bb4ba82149c6768cff3c06c1 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Wed, 18 Sep 2019 16:39:37 +0200
Subject: [PATCH 002/119] [permission] Use full names for permission types
---
apps/permission/models.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index b7cc8845..73000cbd 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -54,10 +54,10 @@ class InstancedPermission:
class Permission(models.Model):
PERMISSION_TYPES = [
- ('C', 'add'),
- ('R', 'view'),
- ('U', 'change'),
- ('D', 'delete')
+ ('add', 'add'),
+ ('view', 'view'),
+ ('change', 'change'),
+ ('delete', 'delete')
]
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
@@ -72,7 +72,7 @@ class Permission(models.Model):
unique_together = ('model', 'permission', 'type', 'field')
def clean(self):
- if self.field and self.type not in {'R', 'U'}:
+ if self.field and self.type not in {'view', 'change'}:
raise ValidationError(_("Specifying field applies only to view and change permission types."))
def save(self):
From d826dc9d2076fcc032eafaf9cbe80f38a5c8f2f1 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Wed, 18 Sep 2019 16:40:21 +0200
Subject: [PATCH 003/119] [permission] Permission admin view
---
apps/permission/admin.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/apps/permission/admin.py b/apps/permission/admin.py
index 8c38f3f3..4594468d 100644
--- a/apps/permission/admin.py
+++ b/apps/permission/admin.py
@@ -1,3 +1,11 @@
from django.contrib import admin
-# Register your models here.
+from .models import Permission
+
+
+@admin.register(Permission)
+class PermissionAdmin(admin.ModelAdmin):
+ """
+ Admin customisation for Permission
+ """
+ list_display = ('type', 'model', 'field', 'permission')
From 1ac63cbed1981005e4bf2b6a0518177b41b81598 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Wed, 18 Sep 2019 16:41:01 +0200
Subject: [PATCH 004/119] [member] Handle unlimited memberships
---
apps/member/models.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/apps/member/models.py b/apps/member/models.py
index 2e84dc75..d1c3dea2 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -146,7 +146,10 @@ class Membership(models.Model):
)
def valid(self):
- return self.date_start <= datetime.datetime.now() < self.date_end
+ if self.date_end is not None:
+ return self.date_start <= datetime.datetime.now() < self.date_end
+ else:
+ return self.date_start <= datetime.datetime.now()
class Meta:
verbose_name = _('membership')
From 3766a1905df1abff26affb74cae5ecd41e194446 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Wed, 18 Sep 2019 16:44:04 +0200
Subject: [PATCH 005/119] [permission] track migrations directory
---
apps/permission/migrations/__init__.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 apps/permission/migrations/__init__.py
diff --git a/apps/permission/migrations/__init__.py b/apps/permission/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
From 94c3a994470efc9f8d58b428a2ccda5caca0b5b9 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sun, 9 Feb 2020 17:35:15 +0100
Subject: [PATCH 006/119] [permission] Rewrite with comments
---
apps/permission/models.py | 87 +++++++++++++++++++++++++--------------
1 file changed, 55 insertions(+), 32 deletions(-)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 73000cbd..b75496ab 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -1,4 +1,6 @@
+import functools
import json
+import operator
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
@@ -9,15 +11,19 @@ from django.utils.translation import gettext_lazy as _
class InstancedPermission:
- def __init__(self, model, permission, type, field):
+ def __init__(self, model, query, type, field):
self.model = model
- self.permission = permission
+ self.query = query
self.type = type
self.field = field
def applies(self, obj, permission_type, field_name=None):
+ """
+ Returns True if the permission applies to
+ the field `field_name` object `obj`
+ """
if ContentType.objects.get_for_model(obj) != self.model:
- # The permission does not apply to the object
+ # The permission does not apply to the model
return False
if self.permission is None:
if permission_type == self.type:
@@ -27,22 +33,10 @@ class InstancedPermission:
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
+ elif obj in self.model.objects.get(self.query):
+ return True
+ else:
+ return False
def __repr__(self):
if self.field:
@@ -62,11 +56,24 @@ class Permission(models.Model):
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
+ # A json encoded Q object with the following grammar
+ # permission -> [] | {} (the empty permission representing all objects)
+ # permission -> ['AND', permission, …]
+ # -> ['OR', permission, …]
+ # -> ['NOT', permission]
+ # permission -> {key: value, …}
+ # key -> string
+ # value -> int | string | bool | null
+ # -> [parameter]
+ #
+ # Examples:
+ # Q(is_admin=True) := {'is_admin': ['TYPE', 'bool', 'True']}
+ # ~Q(is_admin=True) := ['NOT', {'is_admin': ['TYPE', 'bool', 'True']}]
permission = models.TextField()
- type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
+ type = models.CharField(max_length=16, choices=PERMISSION_TYPES)
- field = models.CharField(max_length=255, blank=True)
+ field = models.CharField(max_length=256, blank=True)
class Meta:
unique_together = ('model', 'permission', 'type', 'field')
@@ -80,22 +87,38 @@ class Permission(models.Model):
super().save()
def _about(_self, _permission, **kwargs):
- if _permission[0] == 'all':
+ self = _self
+ permission = _permission
+ if len(permission) == 0:
+ # The permission is either [] or {} and
+ # applies to all objects of the model
+ # to represent this we return None
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]}
+ if isinstance(permission, list):
+ if permission[0] == 'AND':
+ return functools.reduce(operator.and_, [self._about(permission, **kwargs) for permission in permission[1:]])
+ elif permission[0] == 'OR':
+ return functools.reduce(operator.or_, [self._about(permission, **kwargs) for permission in permission[1:]])
+ elif permission[0] == 'NOT':
+ return ~self._about(permission[1], **kwargs)
+ elif isinstance(permission, dict):
+ q_kwargs = {}
+ for key in permission:
+ value = permission[key]
+ if isinstance(value, list):
+ # It is a parameter we query its primary key
+ q_kwargs[key] = kwargs[value[0]].pk
+ else:
+ q_kwargs[key] = value
+ return Q(**q_kwargs)
else:
- return _permission
+ # TODO: find a better way to crash here
+ raise Exception("Permission {} is wrong".format(self.permission))
def about(self, **kwargs):
permission = json.loads(self.permission)
- permission = self._about(permission, **kwargs)
- return InstancedPermission(self.model, permission, self.type, self.field)
+ query = self._about(permission, **kwargs)
+ return InstancedPermission(self.model, query, self.type, self.field)
def __str__(self):
if self.field:
From 72955ae2d6e58901775a43707716e2141dd23eb7 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sun, 9 Feb 2020 18:14:36 +0100
Subject: [PATCH 007/119] [permission] Renamed Permission.permission and added
description field
---
apps/permission/models.py | 68 +++++++++++++++++++++------------------
1 file changed, 37 insertions(+), 31 deletions(-)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index b75496ab..5c016806 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -57,26 +57,28 @@ class Permission(models.Model):
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
# A json encoded Q object with the following grammar
- # permission -> [] | {} (the empty permission representing all objects)
- # permission -> ['AND', permission, …]
- # -> ['OR', permission, …]
- # -> ['NOT', permission]
- # permission -> {key: value, …}
- # key -> string
- # value -> int | string | bool | null
- # -> [parameter]
+ # query -> [] | {} (the empty query representing all objects)
+ # query -> ['AND', query, …]
+ # -> ['OR', query, …]
+ # -> ['NOT', query]
+ # query -> {key: value, …}
+ # key -> string
+ # value -> int | string | bool | null
+ # -> [parameter]
#
# Examples:
# Q(is_admin=True) := {'is_admin': ['TYPE', 'bool', 'True']}
# ~Q(is_admin=True) := ['NOT', {'is_admin': ['TYPE', 'bool', 'True']}]
- permission = models.TextField()
+ query = models.TextField()
- type = models.CharField(max_length=16, choices=PERMISSION_TYPES)
+ type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
- field = models.CharField(max_length=256, blank=True)
+ field = models.CharField(max_length=255, blank=True)
+
+ description = models.CharField(max_length=255, blank=True)
class Meta:
- unique_together = ('model', 'permission', 'type', 'field')
+ unique_together = ('model', 'query', 'type', 'field')
def clean(self):
if self.field and self.type not in {'view', 'change'}:
@@ -86,25 +88,25 @@ class Permission(models.Model):
self.full_clean()
super().save()
- def _about(_self, _permission, **kwargs):
+ def _about(_self, _query, **kwargs):
self = _self
- permission = _permission
- if len(permission) == 0:
- # The permission is either [] or {} and
+ query = _query
+ 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
- if isinstance(permission, list):
- if permission[0] == 'AND':
- return functools.reduce(operator.and_, [self._about(permission, **kwargs) for permission in permission[1:]])
- elif permission[0] == 'OR':
- return functools.reduce(operator.or_, [self._about(permission, **kwargs) for permission in permission[1:]])
- elif permission[0] == 'NOT':
- return ~self._about(permission[1], **kwargs)
- elif isinstance(permission, dict):
+ if isinstance(query, list):
+ if query[0] == 'AND':
+ return functools.reduce(operator.and_, [self._about(query, **kwargs) for query in query[1:]])
+ elif query[0] == 'OR':
+ return functools.reduce(operator.or_, [self._about(query, **kwargs) for query in query[1:]])
+ elif query[0] == 'NOT':
+ return ~self._about(query[1], **kwargs)
+ elif isinstance(query, dict):
q_kwargs = {}
- for key in permission:
- value = permission[key]
+ 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
@@ -113,18 +115,22 @@ class Permission(models.Model):
return Q(**q_kwargs)
else:
# TODO: find a better way to crash here
- raise Exception("Permission {} is wrong".format(self.permission))
+ raise Exception("query {} is wrong".format(self.query))
def about(self, **kwargs):
- permission = json.loads(self.permission)
- query = self._about(permission, **kwargs)
+ """
+ Return an InstancedPermission with the parameters
+ replaced by their values and the query interpreted
+ """
+ query = json.loads(self.query)
+ query = self._about(query, **kwargs)
return InstancedPermission(self.model, query, 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)
+ return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
else:
- return _("Can {type} {model} in {permission}").format(type=self.type, model=self.model, permission=self.permission)
+ return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
class UserPermission(models.Model):
From 2b49effebbf3e09bc0cfb2bd64a7a8f56cebc309 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sun, 9 Feb 2020 18:30:37 +0100
Subject: [PATCH 008/119] [permission] Update admin
---
apps/permission/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/permission/admin.py b/apps/permission/admin.py
index 4594468d..e93de0c5 100644
--- a/apps/permission/admin.py
+++ b/apps/permission/admin.py
@@ -8,4 +8,4 @@ class PermissionAdmin(admin.ModelAdmin):
"""
Admin customisation for Permission
"""
- list_display = ('type', 'model', 'field', 'permission')
+ list_display = ('type', 'model', 'field', 'description')
From 982a5ae0099eca14d58dcb2cb0f9d50d88c10934 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Thu, 13 Feb 2020 15:59:19 +0100
Subject: [PATCH 009/119] [permission] Add F object support
---
apps/permission/models.py | 46 ++++++++++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 8 deletions(-)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 5c016806..2ca17e4c 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -5,7 +5,7 @@ import operator
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.db.models import F, Q
from django.utils.translation import gettext_lazy as _
@@ -58,13 +58,20 @@ class Permission(models.Model):
# A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects)
- # query -> ['AND', query, …]
- # -> ['OR', query, …]
- # -> ['NOT', query]
- # query -> {key: value, …}
- # key -> string
- # value -> int | string | bool | null
- # -> [parameter]
+ # query -> ['AND', query, …] AND multiple queries
+ # | ['OR', query, …] OR multiple queries
+ # | ['NOT', query] Opposite of query
+ # 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
+ # | {'F': oper} An F object
+ # oper -> [string] A parameter
+ # | ['ADD', oper, …] Sum multiple F objects or literal
+ # | ['SUB', oper, oper] Substract two F objects or literal
+ # | ['MUL', oper, …] Multiply F objects or literals
+ # | int | string | bool | null Literal values
+ # | ['F', string] A field
#
# Examples:
# Q(is_admin=True) := {'is_admin': ['TYPE', 'bool', 'True']}
@@ -88,6 +95,26 @@ class Permission(models.Model):
self.full_clean()
super().save()
+ @staticmethod
+ def compute_f(_oper, **kwargs):
+ oper = _oper
+ 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, [compute_f(oper, **kwargs) for oper in oper[1:]])
+ elif oper[0] == 'SUB':
+ return compute_f(oper[1], **kwargs) - compute_f(oper[2], **kwargs)
+ elif oper[0] == 'MUL':
+ return functools.reduce(operator.mul, [compute_f(oper, **kwargs) for oper in oper[1:]])
+ elif oper[0] == 'F':
+ return F(oper[1])
+ else:
+ return oper
+ # TODO: find a better way to crash here
+ raise Exception("F is wrong")
+
def _about(_self, _query, **kwargs):
self = _self
query = _query
@@ -110,6 +137,9 @@ class Permission(models.Model):
if isinstance(value, list):
# It is a parameter we query its primary key
q_kwargs[key] = kwargs[value[0]].pk
+ elif isinstance(value, dict):
+ # It is an F object
+ q_kwargs[key] = compute_f(query['F'], **kwargs)
else:
q_kwargs[key] = value
return Q(**q_kwargs)
From 8a9ad0a6e50ef24c3cc2c1519c2be0f0d955e19c Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sat, 7 Mar 2020 09:30:22 +0100
Subject: [PATCH 010/119] [permission] Handle add rights
---
apps/permission/models.py | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 2ca17e4c..2fcb23cb 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -22,6 +22,9 @@ class InstancedPermission:
Returns True if the permission applies to
the field `field_name` object `obj`
"""
+ if self.type == 'add':
+ if permission_type == self.type:
+ return self.query(obj)
if ContentType.objects.get_for_model(obj) != self.model:
# The permission does not apply to the model
return False
@@ -118,6 +121,9 @@ class Permission(models.Model):
def _about(_self, _query, **kwargs):
self = _self
query = _query
+ if self.type == 'add'):
+ # Handle add permission differently
+ return self._about_add(query, **kwargs)
if len(query) == 0:
# The query is either [] or {} and
# applies to all objects of the model
@@ -147,6 +153,38 @@ class Permission(models.Model):
# TODO: find a better way to crash here
raise Exception("query {} is wrong".format(self.query))
+ def _about_add(_self, _query, **kwargs):
+ self = _self
+ query = _query
+ if len(query) == 0:
+ return lambda _: True
+ if isinstance(query, list):
+ if query[0] == 'AND':
+ return lambda obj: functools.reduce(operator.and_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
+ elif query[0] == 'OR':
+ return lambda obj: functools.reduce(operator.or_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
+ elif query[0] == 'NOT':
+ return lambda obj: not self._about_add(query[1], **kwargs)(obj)
+ elif isinstance(query, dict):
+ q_kwargs = {}
+ 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
+ elif isinstance(value, dict):
+ # It is an F object
+ q_kwargs[key] = compute_f(query['F'], **kwargs)
+ else:
+ q_kwargs[key] = value
+ def func(obj):
+ nonlocal q_kwargs
+ for arg in q_kwargs:
+ if getattr(obj, arg) != q_kwargs(arg):
+ return False
+ return True
+ return func
+
def about(self, **kwargs):
"""
Return an InstancedPermission with the parameters
From 5df1f42f435a7ff0c4e895d06c7de6e45a3baeb0 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sat, 7 Mar 2020 10:48:38 +0100
Subject: [PATCH 011/119] [permission] Syntax error
---
apps/permission/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 2fcb23cb..000fe69f 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -121,7 +121,7 @@ class Permission(models.Model):
def _about(_self, _query, **kwargs):
self = _self
query = _query
- if self.type == 'add'):
+ if self.type == 'add':
# Handle add permission differently
return self._about_add(query, **kwargs)
if len(query) == 0:
From 9d61e217e9994eb3d60d0c53c65af0f294601f84 Mon Sep 17 00:00:00 2001
From: Benjamin Graillot
Date: Sat, 7 Mar 2020 11:21:19 +0100
Subject: [PATCH 012/119] [permission] Only split permission up to 3
---
apps/member/backends.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/member/backends.py b/apps/member/backends.py
index 0b2edad8..9ef9706f 100644
--- a/apps/member/backends.py
+++ b/apps/member/backends.py
@@ -21,7 +21,7 @@ class PermissionBackend(object):
def has_perm(self, user_obj, perm, obj=None):
if obj is None:
return False
- perm = perm.split('_')
+ perm = perm.split('_', 3)
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))
From 30ce17b644c238183f5e0904c1df621dc209d323 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO
Date: Sat, 7 Mar 2020 13:12:17 +0100
Subject: [PATCH 013/119] Update a lot of things
---
apps/logs/signals.py | 4 +++
apps/member/admin.py | 3 +-
apps/member/backends.py | 36 +++++++++++---------
apps/member/models.py | 4 +--
apps/permission/admin.py | 3 ++
apps/permission/apps.py | 3 ++
apps/permission/models.py | 70 ++++++++++++++++++--------------------
apps/permission/tests.py | 3 ++
apps/permission/views.py | 3 ++
note_kfet/settings/base.py | 7 ++--
requirements.txt | 1 -
11 files changed, 75 insertions(+), 62 deletions(-)
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 55e0f041..13194e5b 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -78,6 +78,10 @@ def save_object(sender, instance, **kwargs):
user, ip = get_user_and_ip(sender)
+ from django.contrib.auth.models import AnonymousUser
+ if isinstance(user, AnonymousUser):
+ user = None
+
if user is not None and instance._meta.label_lower == "auth.user" and previous:
# Don't save last login modifications
if instance.last_login != previous.last_login:
diff --git a/apps/member/admin.py b/apps/member/admin.py
index fb107377..70b00459 100644
--- a/apps/member/admin.py
+++ b/apps/member/admin.py
@@ -6,7 +6,7 @@ from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from .forms import ProfileForm
-from .models import Club, Membership, Profile, Role
+from .models import Club, Membership, Profile, Role, RolePermissions
class ProfileInline(admin.StackedInline):
@@ -40,3 +40,4 @@ admin.site.register(User, CustomUserAdmin)
admin.site.register(Club)
admin.site.register(Membership)
admin.site.register(Role)
+admin.site.register(RolePermissions)
diff --git a/apps/member/backends.py b/apps/member/backends.py
index 9ef9706f..db227cdb 100644
--- a/apps/member/backends.py
+++ b/apps/member/backends.py
@@ -1,33 +1,37 @@
-from django.contribs.contenttype.models import ContentType
+# 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.backends import ModelBackend
-class PermissionBackend(object):
+class PermissionBackend(ModelBackend):
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:
+ def permissions(self, user):
+ for membership in Membership.objects.filter(user=user).all():
+ if not membership.valid() or membership.roles 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
+ 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)
+ yield permission
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]
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))
+ return any(permission.applies(obj, perm_type, perm_field) for permission in self.permissions(user_obj))
+
+ def has_module_perms(self, user_obj, app_label):
+ return False
def get_all_permissions(self, user_obj, obj=None):
- if obj is None:
- return []
- else:
- return list(self.permissions(user_obj, obj))
+ return list(self.permissions(user_obj))
diff --git a/apps/member/models.py b/apps/member/models.py
index c90ab15c..1ca82af0 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -154,9 +154,9 @@ class Membership(models.Model):
def valid(self):
if self.date_end is not None:
- return self.date_start <= datetime.datetime.now() < self.date_end
+ return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
else:
- return self.date_start <= datetime.datetime.now()
+ return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
class Meta:
verbose_name = _('membership')
diff --git a/apps/permission/admin.py b/apps/permission/admin.py
index e93de0c5..f7a9b4b5 100644
--- a/apps/permission/admin.py
+++ b/apps/permission/admin.py
@@ -1,3 +1,6 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-lateré
+
from django.contrib import admin
from .models import Permission
diff --git a/apps/permission/apps.py b/apps/permission/apps.py
index 0f46ef08..c9c912a5 100644
--- a/apps/permission/apps.py
+++ b/apps/permission/apps.py
@@ -1,3 +1,6 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
from django.apps import AppConfig
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 000fe69f..9584f59f 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -1,3 +1,6 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
import functools
import json
import operator
@@ -24,28 +27,25 @@ class InstancedPermission:
"""
if self.type == 'add':
if permission_type == self.type:
- return self.query(obj)
+ return obj in self.model.modelclass().objects.get(self.query)
if ContentType.objects.get_for_model(obj) != self.model:
# The permission does not apply to the model
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:
+ if permission_type == self.type:
+ if field_name and field_name != self.field:
return False
- elif obj in self.model.objects.get(self.query):
- return True
+ return obj in self.model.model_class().objects.filter(self.query).all()
else:
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)
+ return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
else:
- return _("Can {type} {model} in {permission}").format(type=self.type, model=self.model, permission=self.permission)
+ return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
+
+ def __str__(self):
+ return self.__repr__()
class Permission(models.Model):
@@ -61,24 +61,24 @@ class Permission(models.Model):
# A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects)
- # query -> ['AND', query, …] AND multiple queries
- # | ['OR', query, …] OR multiple queries
- # | ['NOT', query] Opposite of query
+ # query -> ["AND", query, …] AND multiple queries
+ # | ["OR", query, …] OR multiple queries
+ # | ["NOT", query] Opposite of query
# 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
- # | {'F': oper} An F object
+ # | {"F": oper} An F object
# oper -> [string] A parameter
- # | ['ADD', oper, …] Sum multiple F objects or literal
- # | ['SUB', oper, oper] Substract two F objects or literal
- # | ['MUL', oper, …] Multiply F objects or literals
+ # | ["ADD", oper, …] Sum multiple F objects or literal
+ # | ["SUB", oper, oper] Substract two F objects or literal
+ # | ["MUL", oper, …] Multiply F objects or literals
# | int | string | bool | null Literal values
- # | ['F', string] A field
+ # | ["F", string] A field
#
# Examples:
- # Q(is_admin=True) := {'is_admin': ['TYPE', 'bool', 'True']}
- # ~Q(is_admin=True) := ['NOT', {'is_admin': ['TYPE', 'bool', 'True']}]
+ # Q(is_superuser=True) := {"is_superuser": true}
+ # ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
query = models.TextField()
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
@@ -94,23 +94,22 @@ class Permission(models.Model):
if self.field and self.type not in {'view', 'change'}:
raise ValidationError(_("Specifying field applies only to view and change permission types."))
- def save(self):
+ def save(self, **kwargs):
self.full_clean()
super().save()
@staticmethod
- def compute_f(_oper, **kwargs):
- oper = _oper
+ 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, [compute_f(oper, **kwargs) for oper in oper[1:]])
+ return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
elif oper[0] == 'SUB':
- return compute_f(oper[1], **kwargs) - compute_f(oper[2], **kwargs)
+ return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
elif oper[0] == 'MUL':
- return functools.reduce(operator.mul, [compute_f(oper, **kwargs) for oper in oper[1:]])
+ return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
elif oper[0] == 'F':
return F(oper[1])
else:
@@ -118,9 +117,7 @@ class Permission(models.Model):
# TODO: find a better way to crash here
raise Exception("F is wrong")
- def _about(_self, _query, **kwargs):
- self = _self
- query = _query
+ def _about(self, query, **kwargs):
if self.type == 'add':
# Handle add permission differently
return self._about_add(query, **kwargs)
@@ -145,7 +142,7 @@ class Permission(models.Model):
q_kwargs[key] = kwargs[value[0]].pk
elif isinstance(value, dict):
# It is an F object
- q_kwargs[key] = compute_f(query['F'], **kwargs)
+ q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
else:
q_kwargs[key] = value
return Q(**q_kwargs)
@@ -153,16 +150,15 @@ class Permission(models.Model):
# TODO: find a better way to crash here
raise Exception("query {} is wrong".format(self.query))
- def _about_add(_self, _query, **kwargs):
- self = _self
+ def _about_add(self, _query, **kwargs):
query = _query
if len(query) == 0:
return lambda _: True
if isinstance(query, list):
if query[0] == 'AND':
- return lambda obj: functools.reduce(operator.and_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
+ return lambda obj: functools.reduce(operator.and_, [self._about_add(q, **kwargs)(obj) for q in query[1:]])
elif query[0] == 'OR':
- return lambda obj: functools.reduce(operator.or_, [self._about_add(query, **kwargs)(obj) for query in query[1:]])
+ return lambda obj: functools.reduce(operator.or_, [self._about_add(q, **kwargs)(obj) for q in query[1:]])
elif query[0] == 'NOT':
return lambda obj: not self._about_add(query[1], **kwargs)(obj)
elif isinstance(query, dict):
@@ -174,7 +170,7 @@ class Permission(models.Model):
q_kwargs[key] = kwargs[value[0]].pk
elif isinstance(value, dict):
# It is an F object
- q_kwargs[key] = compute_f(query['F'], **kwargs)
+ q_kwargs[key] = Permission.compute_f(query['F'], **kwargs)
else:
q_kwargs[key] = value
def func(obj):
diff --git a/apps/permission/tests.py b/apps/permission/tests.py
index 7ce503c2..b5d5752e 100644
--- a/apps/permission/tests.py
+++ b/apps/permission/tests.py
@@ -1,3 +1,6 @@
+# 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
index 91ea44a2..8d81fd33 100644
--- a/apps/permission/views.py
+++ b/apps/permission/views.py
@@ -1,3 +1,6 @@
+# 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 63b7ff24..20937fac 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -37,7 +37,6 @@ INSTALLED_APPS = [
# External apps
'polymorphic',
- 'guardian',
'reversion',
'crispy_forms',
'django_tables2',
@@ -134,8 +133,8 @@ PASSWORD_HASHERS = [
# Django Guardian object permissions
AUTHENTICATION_BACKENDS = (
- 'django.contrib.auth.backends.ModelBackend', # this is default
- 'guardian.backends.ObjectPermissionBackend',
+ #'django.contrib.auth.backends.ModelBackend', # this is default
+ 'member.backends.PermissionBackend',
'cas.backends.CASBackend',
)
@@ -153,8 +152,6 @@ REST_FRAMEWORK = {
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
-GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
-
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
diff --git a/requirements.txt b/requirements.txt
index 244690bc..9a5eaa22 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ django-cas-server==1.1.0
django-crispy-forms==1.7.2
django-extensions==2.1.9
django-filter==2.2.0
-django-guardian==2.1.0
django-polymorphic==2.0.3
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8
From 205fd5c17a0bcd83e69e1b2934078a7daea6b6a8 Mon Sep 17 00:00:00 2001
From: Pierre-antoine Comby
Date: Mon, 9 Mar 2020 19:03:36 +0100
Subject: [PATCH 014/119] initiate submodule for scripts
---
.gitmodules | 3 +++
apps/scripts | 1 +
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 apps/scripts
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..94cf1be6
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "apps/scripts"]
+ path = apps/scripts
+ url = git@gitlab.crans.org:bde/nk20-scripts.git
diff --git a/apps/scripts b/apps/scripts
new file mode 160000
index 00000000..123466cf
--- /dev/null
+++ b/apps/scripts
@@ -0,0 +1 @@
+Subproject commit 123466cfa914422422cd372197e64adf65ef05f7
From 462a9e0f2d6f9693917992378370444e74206685 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO
Date: Mon, 9 Mar 2020 22:13:11 +0100
Subject: [PATCH 015/119] Fix CAS settings
---
note_kfet/settings/__init__.py | 5 +++++
note_kfet/settings/base.py | 2 ++
2 files changed, 7 insertions(+)
diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py
index 6d871599..c1df7477 100644
--- a/note_kfet/settings/__init__.py
+++ b/note_kfet/settings/__init__.py
@@ -1,4 +1,9 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.utils.translation import gettext_lazy as _
import re
+
from .base import *
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index 4fe12fbf..3a5c3200 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -39,6 +39,8 @@ INSTALLED_APPS = [
'polymorphic',
'crispy_forms',
'django_tables2',
+ 'cas_server',
+ 'cas',
# Django contrib
'django.contrib.admin',
'django.contrib.admindocs',
From 83b1ad47515b6fa9ce875709b4813bbe92d823ce Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO
Date: Mon, 9 Mar 2020 22:23:47 +0100
Subject: [PATCH 016/119] Multi-connexions
---
note_kfet/urls.py | 4 ++--
templates/registration/login.html | 4 ++++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/note_kfet/urls.py b/note_kfet/urls.py
index 896c0655..407659f8 100644
--- a/note_kfet/urls.py
+++ b/note_kfet/urls.py
@@ -37,8 +37,8 @@ if "cas" in settings.INSTALLED_APPS:
from cas import views as cas_views
urlpatterns += [
# Include CAS Client routers
- path('accounts/login/', cas_views.login, name='login'),
- path('accounts/logout/', cas_views.logout, name='logout'),
+ path('accounts/login/cas/', cas_views.login, name='cas_login'),
+ path('accounts/logout/cas/', cas_views.logout, name='cas_logout'),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
diff --git a/templates/registration/login.html b/templates/registration/login.html
index 04ef8d7d..5a4322d1 100644
--- a/templates/registration/login.html
+++ b/templates/registration/login.html
@@ -17,6 +17,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% endif %}
+
+