diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py index cb32b09e4828ff7e2630dc1c1f563098c551e682..6c5a207a114e0ab1f41323d6f4c6ff777e776a98 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 3fdbd8d1fd52b8ef8f39bb1343331d7416f9837a..f0b4e8f2925ca352e73481920a9b065149c4ba16 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 2213f37dd83c8d92cda1a8b6dcbae5d64f2f749b..293ad3a8c0b6e1aeca4d6d7a555e5a5eb02f83bf 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 6a3bb41ec03be017330c56dad62783c999fd871f..a4fe6fc19ed9afe7ada81b8258bcb50436262904 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 ee890c9d55c1e70ae7516be35065cf1f1c04c93e..b7c8f0929e762dd4d7f7c31d56f21f8ba6a7e969 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 31a79be7c992c8a70899d5b6f4d2d19480fdc1da..6b2cb37266917b2afb832638a857b300cc76618a 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py new file mode 100644 index 0000000000000000000000000000000000000000..9b5ff93abff4c49940b639bdb6ee7d0b91b2590c --- /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 e61937021c6f566f9b3125cc00dac9de9e66634c..fae864433187801b0cabdf6c7c8dfbfab923dbc5 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 </button> <div class="collapse navbar-collapse" id="navbarNavDropdown"> <ul class="navbar-nav"> - <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> - </li> - <li class="nav-item active"> - <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> - </li> - <li class="nav-item active"> - <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> - </li> - <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> - </li> - <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> - </li> + {% if "note.transactiontemplate"|not_empty_model_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> + </li> + {% endif %} + {% if "member.club"|not_empty_model_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> + </li> + {% endif %} + {% if "activity.activity"|not_empty_model_list %} + <li class="nav-item active"> + <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> + </li> + {% endif %} + {% if "note.transactiontemplate"|not_empty_model_change_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> + </li> + {% endif %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> + </li> </ul> <ul class="navbar-nav ml-auto"> {% if user.is_authenticated %}