Implements permission masks

This commit is contained in:
Yohann D'ANELLO 2020-03-19 16:12:52 +01:00
parent d083894e9b
commit 95315cdbe2
15 changed files with 133 additions and 78 deletions

View File

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

View File

@ -9,7 +9,7 @@ import getpass
from note.models import NoteUser, Alias 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 from .models import Changelog

View File

@ -3,10 +3,10 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db.models import Q, F from django.db.models import Q, F
from note.models import Note, NoteUser, NoteClub, NoteSpecial from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet.middlewares import get_current_session
from .models import Membership, RolePermissions, Club from .models import Membership, RolePermissions, Club
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
@ -37,6 +37,7 @@ class PermissionBackend(ModelBackend):
F=F, F=F,
Q=Q Q=Q
) )
if permission.mask.rank <= get_current_session().get("permission_mask", 0):
yield permission yield permission
@staticmethod @staticmethod
@ -50,7 +51,7 @@ class PermissionBackend(ModelBackend):
:return: A query that corresponds to the filter to give to a queryset :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 # Superusers have all rights
return Q() return Q()
@ -68,7 +69,7 @@ class PermissionBackend(ModelBackend):
return query return query
def has_perm(self, user_obj, perm, obj=None): 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 return True
if obj is None: if obj is None:

View File

@ -6,12 +6,21 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout from crispy_forms.layout import Layout
from dal import autocomplete from dal import autocomplete
from django import forms 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 django.contrib.auth.models import User
from permission.models import PermissionMask
from .models import Profile, Club, Membership 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): class SignUpForm(UserCreationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -9,6 +9,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
@ -26,11 +27,20 @@ from note.tables import HistoryTable, AliasTable
from .backends import PermissionBackend from .backends import PermissionBackend
from .filters import UserFilter, UserFilterFormHelper 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 .models import Club, Membership
from .tables import ClubTable, UserTable 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): class UserCreateView(CreateView):
""" """
Une vue pour inscrire un utilisateur et lui créer un profile Une vue pour inscrire un utilisateur et lui créer un profile

View File

@ -4,7 +4,7 @@
from rest_framework import serializers from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer 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.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
TemplateTransaction, SpecialTransaction TemplateTransaction, SpecialTransaction

View File

@ -3,7 +3,7 @@
"model": "note.note", "model": "note.note",
"pk": 1, "pk": 1,
"fields": { "fields": {
"polymorphic_ctype": 40, "polymorphic_ctype": 41,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -14,7 +14,7 @@
"model": "note.note", "model": "note.note",
"pk": 2, "pk": 2,
"fields": { "fields": {
"polymorphic_ctype": 40, "polymorphic_ctype": 41,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -25,7 +25,7 @@
"model": "note.note", "model": "note.note",
"pk": 3, "pk": 3,
"fields": { "fields": {
"polymorphic_ctype": 40, "polymorphic_ctype": 41,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -36,7 +36,7 @@
"model": "note.note", "model": "note.note",
"pk": 4, "pk": 4,
"fields": { "fields": {
"polymorphic_ctype": 40, "polymorphic_ctype": 41,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -47,7 +47,7 @@
"model": "note.note", "model": "note.note",
"pk": 5, "pk": 5,
"fields": { "fields": {
"polymorphic_ctype": 39, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -58,7 +58,7 @@
"model": "note.note", "model": "note.note",
"pk": 6, "pk": 6,
"fields": { "fields": {
"polymorphic_ctype": 39, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",

View File

@ -3,7 +3,15 @@
from django.contrib import admin 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) @admin.register(Permission)

View File

@ -50,6 +50,20 @@ class InstancedPermission:
return self.__repr__() 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): class Permission(models.Model):
PERMISSION_TYPES = [ PERMISSION_TYPES = [
@ -85,6 +99,11 @@ class Permission(models.Model):
type = models.CharField(max_length=15, choices=PERMISSION_TYPES) 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) field = models.CharField(max_length=255, blank=True)
description = models.CharField(max_length=255, blank=True) description = models.CharField(max_length=255, blank=True)

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from logs.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_authenticated_user
EXCLUDED = [ EXCLUDED = [

View File

@ -4,7 +4,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter 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 django import template
from member.backends import PermissionBackend from member.backends import PermissionBackend
@ -19,7 +19,7 @@ def not_empty_model_list(model_name):
user = get_current_authenticated_user() user = get_current_authenticated_user()
if user is None: if user is None:
return False return False
elif user.is_superuser: elif user.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
return True return True
spl = model_name.split(".") spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) 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() user = get_current_authenticated_user()
if user is None: if user is None:
return False return False
elif user.is_superuser: elif user.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
return True return True
spl = model_name.split(".") spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) ct = ContentType.objects.get(app_label=spl[0], model=spl[1])

View File

@ -7,7 +7,7 @@ if [ -z ${NOTE_URL+x} ]; then
else else
sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json 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/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 sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json
fi fi

View File

@ -1,6 +1,66 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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): class TurbolinksMiddleware(object):
""" """

View File

@ -74,7 +74,7 @@ if "cas" in INSTALLED_APPS:
if "logs" in INSTALLED_APPS: if "logs" in INSTALLED_APPS:
MIDDLEWARE += ('logs.middlewares.LogsMiddleware',) MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
if "debug_toolbar" in INSTALLED_APPS: if "debug_toolbar" in INSTALLED_APPS:
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")

View File

@ -7,6 +7,8 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.generic import RedirectView from django.views.generic import RedirectView
from member.views import CustomLoginView
urlpatterns = [ urlpatterns = [
# Dev so redirect to something random # Dev so redirect to something random
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
@ -16,10 +18,11 @@ urlpatterns = [
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), 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/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.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')), path('api/', include('api.urls')),
] ]