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