diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py index cb32b09e..6c5a207a 100644 --- a/apps/api/viewsets.py +++ b/apps/api/viewsets.py @@ -13,7 +13,7 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet): def get_queryset(self): model = ContentType.objects.get_for_model(self.serializer_class.Meta.model) - return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view")) + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, model, "view")) class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): @@ -23,4 +23,4 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): model = ContentType.objects.get_for_model(self.serializer_class.Meta.model) - return super().get_queryset().filter(PermissionBackend().filter_queryset(self.request.user, model, "view")) + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, model, "view")) diff --git a/apps/member/backends.py b/apps/member/backends.py index 3fdbd8d1..f0b4e8f2 100644 --- a/apps/member/backends.py +++ b/apps/member/backends.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later 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 @@ -15,7 +16,8 @@ class PermissionBackend(ModelBackend): supports_anonymous_user = False supports_inactive_user = False - def permissions(self, user): + @staticmethod + def permissions(user): for membership in Membership.objects.filter(user=user).all(): if not membership.valid() or membership.roles is None: continue @@ -37,12 +39,13 @@ class PermissionBackend(ModelBackend): ) yield permission - def filter_queryset(self, user, model, type, field=None): + @staticmethod + def filter_queryset(user, model, t, field=None): """ Filter a queryset by considering the permissions of a given user. :param user: The owner of the permissions that are fetched :param model: The concerned model of the queryset - :param type: The type of modification (view, add, change, delete) + :param t: The type of modification (view, add, change, delete) :param field: The field of the model to test, if concerned :return: A query that corresponds to the filter to give to a queryset """ @@ -51,12 +54,15 @@ class PermissionBackend(ModelBackend): # Superusers have all rights return Q() + if not isinstance(model, ContentType): + model = ContentType.objects.get_for_model(model) + # Never satisfied query = Q(pk=-1) - for perm in self.permissions(user): - if field and field != perm.field: + for perm in PermissionBackend.permissions(user): + if perm.field and field != perm.field: continue - if perm.model != model or perm.type != type: + if perm.model != model or perm.type != t: continue query = query | perm.query return query diff --git a/apps/member/views.py b/apps/member/views.py index 2213f37d..293ad3a8 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -23,6 +23,7 @@ from note.forms import AliasForm, ImageForm from note.models import Alias, NoteUser from note.models.transactions import Transaction from note.tables import HistoryTable, AliasTable +from .backends import PermissionBackend from .filters import UserFilter, UserFilterFormHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper @@ -120,6 +121,9 @@ class UserDetailView(LoginRequiredMixin, DetailView): context_object_name = "user_object" template_name = "member/profile_detail.html" + def get_queryset(self, **kwargs): + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = context['user_object'] @@ -147,7 +151,7 @@ class UserListView(LoginRequiredMixin, SingleTableView): formhelper_class = UserFilterFormHelper def get_queryset(self, **kwargs): - qs = super().get_queryset() + qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) self.filter = self.filter_class(self.request.GET, queryset=qs) self.filter.form.helper = self.formhelper_class() return self.filter.qs @@ -296,7 +300,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView): if not self.request.user.is_authenticated: return User.objects.none() - qs = User.objects.all() + qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all() if self.q: qs = qs.filter(username__regex="^" + self.q) @@ -327,11 +331,17 @@ class ClubListView(LoginRequiredMixin, SingleTableView): model = Club table_class = ClubTable + def get_queryset(self, **kwargs): + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) + class ClubDetailView(LoginRequiredMixin, DetailView): model = Club context_object_name = "club" + def get_queryset(self, **kwargs): + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = context["club"] @@ -350,6 +360,11 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): form_class = MembershipForm template_name = 'member/add_members.html' + def get_queryset(self, **kwargs): + return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") + | PermissionBackend.filter_queryset(self.request.user, Membership, + "change")) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['formset'] = MemberFormSet() diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 6a3bb41e..a4fe6fc1 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter, SearchFilter from api.viewsets import ReadProtectedModelViewSet +from member.backends import PermissionBackend from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ NoteUserSerializer, AliasSerializer, \ TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer @@ -70,7 +71,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet): Parse query and apply filters. :return: The filtered set of requested notes """ - queryset = super().get_queryset() + queryset = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Note, "view")) alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( @@ -110,7 +111,7 @@ class AliasViewSet(ReadProtectedModelViewSet): :return: The filtered set of requested aliases """ - queryset = super().get_queryset() + queryset = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")) alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index ee890c9d..b7c8f092 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -129,13 +129,14 @@ class Transaction(PolymorphicModel): models.Index(fields=['destination']), ] - def post_save(self, *args, **kwargs): + def save(self, *args, **kwargs): """ When saving, also transfer money between two notes """ if self.source.pk == self.destination.pk: # When source == destination, no money is transfered + super().save(*args, **kwargs) return created = self.pk is None diff --git a/apps/note/views.py b/apps/note/views.py index 31a79be7..6b2cb372 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, ListView, UpdateView from django_tables2 import SingleTableView +from member.backends import PermissionBackend from .forms import TransactionTemplateForm from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial from .models.transactions import SpecialTransaction @@ -18,16 +19,18 @@ from .tables import HistoryTable class TransactionCreate(LoginRequiredMixin, SingleTableView): """ Show transfer page - - TODO: If user have sufficient rights, they can transfer from an other note """ - queryset = Transaction.objects.order_by("-id").all()[:50] template_name = "note/transaction_form.html" # Transaction history table table_class = HistoryTable table_pagination = {"per_page": 50} + def get_queryset(self): + return Transaction.objects.filter(PermissionBackend + .filter_queryset(self.request.user, Transaction, "view")) \ + .order_by("-id").all()[:50] + def get_context_data(self, **kwargs): """ Add some context variables in template such as page title @@ -117,21 +120,26 @@ class ConsoView(LoginRequiredMixin, SingleTableView): """ Consume """ - queryset = Transaction.objects.order_by("-id").all()[:50] template_name = "note/conso_form.html" # Transaction history table table_class = HistoryTable table_pagination = {"per_page": 50} + def get_queryset(self): + return Transaction.objects.filter(PermissionBackend + .filter_queryset(self.request.user, Transaction, "view")) \ + .order_by("-id").all()[:50] + def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) from django.db.models import Count - buttons = TransactionTemplate.objects.filter(display=True) \ - .annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') + buttons = TransactionTemplate.objects.filter(PermissionBackend() + .filter_queryset(self.request.user, TransactionTemplate, "view")) \ + .filter(display=True).annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') context['transaction_templates'] = buttons context['most_used'] = buttons.order_by('-clicks', 'name')[:10] context['title'] = _("Consumptions") diff --git a/apps/permission/templatetags/__init__.py b/apps/permission/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py new file mode 100644 index 00000000..9b5ff93a --- /dev/null +++ b/apps/permission/templatetags/perms.py @@ -0,0 +1,42 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.contenttypes.models import ContentType +from django.template.defaultfilters import stringfilter + +from logs.middlewares import get_current_authenticated_user +from django import template + +from member.backends import PermissionBackend + + +def has_perm(value): + return get_current_authenticated_user().has_perm(value) + + +@stringfilter +def not_empty_model_list(model_name): + user = get_current_authenticated_user() + if user.is_superuser: + return True + spl = model_name.split(".") + ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) + qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")) + return qs.exists() + + +@stringfilter +def not_empty_model_change_list(model_name): + user = get_current_authenticated_user() + if user.is_superuser: + return True + spl = model_name.split(".") + ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) + qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change")) + return qs.exists() + + +register = template.Library() +register.filter('has_perm', has_perm) +register.filter('not_empty_model_list', not_empty_model_list) +register.filter('not_empty_model_change_list', not_empty_model_change_list) diff --git a/templates/base.html b/templates/base.html index e6193702..fae86443 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,4 @@ -{% load static i18n pretty_money static getenv %} +{% load static i18n pretty_money static getenv perms %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} @@ -74,21 +74,29 @@ SPDX-License-Identifier: GPL-3.0-or-later