From 95315cdbe27315cefe0eca739cdb1d09b0aae331 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 19 Mar 2020 16:12:52 +0100 Subject: [PATCH] Implements permission masks --- apps/logs/middlewares.py | 55 ------------------------ apps/logs/signals.py | 2 +- apps/member/backends.py | 9 ++-- apps/member/forms.py | 11 ++++- apps/member/views.py | 12 +++++- apps/note/api/serializers.py | 2 +- apps/note/fixtures/initial.json | 12 +++--- apps/permission/admin.py | 10 ++++- apps/permission/models.py | 19 +++++++++ apps/permission/signals.py | 2 +- apps/permission/templatetags/perms.py | 6 +-- entrypoint.sh | 2 +- note_kfet/middlewares.py | 60 +++++++++++++++++++++++++++ note_kfet/settings/__init__.py | 2 +- note_kfet/urls.py | 7 +++- 15 files changed, 133 insertions(+), 78 deletions(-) delete mode 100644 apps/logs/middlewares.py diff --git a/apps/logs/middlewares.py b/apps/logs/middlewares.py deleted file mode 100644 index 77f749b9..00000000 --- a/apps/logs/middlewares.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.conf import settings -from django.contrib.auth.models import AnonymousUser - -from threading import local - - -USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') -IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') - -_thread_locals = local() - - -def _set_current_user_and_ip(user=None, ip=None): - setattr(_thread_locals, USER_ATTR_NAME, user) - setattr(_thread_locals, IP_ATTR_NAME, ip) - - -def get_current_user(): - return getattr(_thread_locals, USER_ATTR_NAME, None) - - -def get_current_ip(): - return getattr(_thread_locals, IP_ATTR_NAME, None) - - -def get_current_authenticated_user(): - current_user = get_current_user() - if isinstance(current_user, AnonymousUser): - return None - return current_user - - -class LogsMiddleware(object): - """ - This middleware get the current user with his or her IP address on each request. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - user = request.user - if 'HTTP_X_FORWARDED_FOR' in request.META: - ip = request.META.get('HTTP_X_FORWARDED_FOR') - else: - ip = request.META.get('REMOTE_ADDR') - - _set_current_user_and_ip(user, ip) - response = self.get_response(request) - _set_current_user_and_ip(None, None) - - return response diff --git a/apps/logs/signals.py b/apps/logs/signals.py index fb17157a..0c80a4cd 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -9,7 +9,7 @@ import getpass from note.models import NoteUser, Alias -from .middlewares import get_current_authenticated_user, get_current_ip +from note_kfet.middlewares import get_current_authenticated_user, get_current_ip from .models import Changelog diff --git a/apps/member/backends.py b/apps/member/backends.py index f0b4e8f2..e68f6c19 100644 --- a/apps/member/backends.py +++ b/apps/member/backends.py @@ -3,10 +3,10 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied from django.db.models import Q, F from note.models import Note, NoteUser, NoteClub, NoteSpecial +from note_kfet.middlewares import get_current_session from .models import Membership, RolePermissions, Club from django.contrib.auth.backends import ModelBackend @@ -37,7 +37,8 @@ class PermissionBackend(ModelBackend): F=F, Q=Q ) - yield permission + if permission.mask.rank <= get_current_session().get("permission_mask", 0): + yield permission @staticmethod def filter_queryset(user, model, t, field=None): @@ -50,7 +51,7 @@ class PermissionBackend(ModelBackend): :return: A query that corresponds to the filter to give to a queryset """ - if user.is_superuser: + if user.is_superuser and get_current_session().get("permission_mask", 0) >= 42: # Superusers have all rights return Q() @@ -68,7 +69,7 @@ class PermissionBackend(ModelBackend): return query def has_perm(self, user_obj, perm, obj=None): - if user_obj.is_superuser: + if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42: return True if obj is None: diff --git a/apps/member/forms.py b/apps/member/forms.py index d2134cdd..0f1ff189 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -6,12 +6,21 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout from dal import autocomplete from django import forms -from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.models import User +from permission.models import PermissionMask from .models import Profile, Club, Membership +class CustomAuthenticationForm(AuthenticationForm): + permission_mask = forms.ModelChoiceField( + label="Masque de permissions", + queryset=PermissionMask.objects.order_by("rank"), + empty_label=None, + ) + + class SignUpForm(UserCreationForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/apps/member/views.py b/apps/member/views.py index 293ad3a8..3b19503b 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -9,6 +9,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User +from django.contrib.auth.views import LoginView from django.core.exceptions import ValidationError from django.db.models import Q from django.http import HttpResponseRedirect @@ -26,11 +27,20 @@ from note.tables import HistoryTable, AliasTable from .backends import PermissionBackend from .filters import UserFilter, UserFilterFormHelper -from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper +from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ + CustomAuthenticationForm from .models import Club, Membership from .tables import ClubTable, UserTable +class CustomLoginView(LoginView): + form_class = CustomAuthenticationForm + + def form_valid(self, form): + self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank + return super().form_valid(form) + + class UserCreateView(CreateView): """ Une vue pour inscrire un utilisateur et lui créer un profile diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 4d8be07f..36696024 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer -from logs.middlewares import get_current_authenticated_user +from note_kfet.middlewares import get_current_authenticated_user from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ TemplateTransaction, SpecialTransaction diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 3654fa2f..eac2bda1 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -3,7 +3,7 @@ "model": "note.note", "pk": 1, "fields": { - "polymorphic_ctype": 40, + "polymorphic_ctype": 41, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 40, + "polymorphic_ctype": 41, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 40, + "polymorphic_ctype": 41, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 40, + "polymorphic_ctype": 41, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", diff --git a/apps/permission/admin.py b/apps/permission/admin.py index f7a9b4b5..2e6899fd 100644 --- a/apps/permission/admin.py +++ b/apps/permission/admin.py @@ -3,7 +3,15 @@ from django.contrib import admin -from .models import Permission +from .models import Permission, PermissionMask + + +@admin.register(PermissionMask) +class PermissionMaskAdmin(admin.ModelAdmin): + """ + Admin customisation for Permission + """ + list_display = ('rank', 'description') @admin.register(Permission) diff --git a/apps/permission/models.py b/apps/permission/models.py index ead3f721..f333e377 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -50,6 +50,20 @@ class InstancedPermission: return self.__repr__() +class PermissionMask(models.Model): + rank = models.PositiveSmallIntegerField( + verbose_name=_('rank'), + ) + + description = models.CharField( + max_length=255, + verbose_name=_('description'), + ) + + def __str__(self): + return self.description + + class Permission(models.Model): PERMISSION_TYPES = [ @@ -85,6 +99,11 @@ class Permission(models.Model): type = models.CharField(max_length=15, choices=PERMISSION_TYPES) + mask = models.ForeignKey( + PermissionMask, + on_delete=models.PROTECT, + ) + field = models.CharField(max_length=255, blank=True) description = models.CharField(max_length=255, blank=True) diff --git a/apps/permission/signals.py b/apps/permission/signals.py index e93c1666..6d4f5f19 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.core.exceptions import PermissionDenied -from logs.middlewares import get_current_authenticated_user +from note_kfet.middlewares import get_current_authenticated_user EXCLUDED = [ diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 460bf9a6..f65b606e 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import stringfilter -from logs.middlewares import get_current_authenticated_user +from note_kfet.middlewares import get_current_authenticated_user, get_current_session from django import template from member.backends import PermissionBackend @@ -19,7 +19,7 @@ def not_empty_model_list(model_name): user = get_current_authenticated_user() if user is None: return False - elif user.is_superuser: + elif user.is_superuser and get_current_session().get("permission_mask", 0) >= 42: return True spl = model_name.split(".") ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) @@ -32,7 +32,7 @@ def not_empty_model_change_list(model_name): user = get_current_authenticated_user() if user is None: return False - elif user.is_superuser: + elif user.is_superuser and get_current_session().get("permission_mask", 0) >= 42: return True spl = model_name.split(".") ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) diff --git a/entrypoint.sh b/entrypoint.sh index e5a22a5a..4d0177e8 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,7 +7,7 @@ if [ -z ${NOTE_URL+x} ]; then else sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json sed -i -e "s/localhost/$NOTE_URL/g" /code/note_kfet/fixtures/initial.json - sed -i -e "s/\.\*/https?:\/\/$NOTE_URL\/.*/g" /code/note_kfet/fixtures/cas.json + sed -i -e "s/\"\.\*\"/\"https?:\/\/$NOTE_URL\/.*\"/g" /code/note_kfet/fixtures/cas.json sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json fi diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py index b034e2be..fff824c5 100644 --- a/note_kfet/middlewares.py +++ b/note_kfet/middlewares.py @@ -1,6 +1,66 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from django.conf import settings +from django.contrib.auth.models import AnonymousUser, User + +from threading import local + +from django.contrib.sessions.backends.db import SessionStore + +USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') +SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') +IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') + +_thread_locals = local() + + +def _set_current_user_and_ip(user=None, session=None, ip=None): + setattr(_thread_locals, USER_ATTR_NAME, user) + setattr(_thread_locals, SESSION_ATTR_NAME, session) + setattr(_thread_locals, IP_ATTR_NAME, ip) + + +def get_current_user() -> User: + return getattr(_thread_locals, USER_ATTR_NAME, None) + + +def get_current_session() -> SessionStore: + return getattr(_thread_locals, SESSION_ATTR_NAME, None) + + +def get_current_ip() -> str: + return getattr(_thread_locals, IP_ATTR_NAME, None) + + +def get_current_authenticated_user(): + current_user = get_current_user() + if isinstance(current_user, AnonymousUser): + return None + return current_user + + +class SessionMiddleware(object): + """ + This middleware get the current user with his or her IP address on each request. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + user = request.user + if 'HTTP_X_FORWARDED_FOR' in request.META: + ip = request.META.get('HTTP_X_FORWARDED_FOR') + else: + ip = request.META.get('REMOTE_ADDR') + + _set_current_user_and_ip(user, request.session, ip) + response = self.get_response(request) + _set_current_user_and_ip(None, None, None) + + return response + class TurbolinksMiddleware(object): """ diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 28935deb..7370f1bf 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -74,7 +74,7 @@ if "cas" in INSTALLED_APPS: if "logs" in INSTALLED_APPS: - MIDDLEWARE += ('logs.middlewares.LogsMiddleware',) + MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) if "debug_toolbar" in INSTALLED_APPS: MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/note_kfet/urls.py b/note_kfet/urls.py index da2f9d6c..9170c62e 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -7,6 +7,8 @@ from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView +from member.views import CustomLoginView + urlpatterns = [ # Dev so redirect to something random path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), @@ -16,10 +18,11 @@ urlpatterns = [ # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), - path('accounts/', include('member.urls')), - path('accounts/', include('django.contrib.auth.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), + path('accounts/', include('member.urls')), + path('accounts/login/', CustomLoginView.as_view()), + path('accounts/', include('django.contrib.auth.urls')), path('api/', include('api.urls')), ]