From 95315cdbe27315cefe0eca739cdb1d09b0aae331 Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <yohann.danello@gmail.com>
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')),
 ]
 
-- 
GitLab