From 1aae18e6a66f0bd2993dec89fdc97ff2724be7ce Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 31 Mar 2020 04:16:30 +0200 Subject: [PATCH] Improved permissions, 404 and 403 errors will be more frequent (when we type an invalid URL) --- apps/activity/views.py | 24 +++-- apps/member/forms.py | 8 +- apps/member/views.py | 83 +++++++-------- apps/note/models/__init__.py | 4 +- apps/note/views.py | 30 +++--- apps/permission/backends.py | 5 +- apps/permission/fixtures/initial.json | 134 ++++++++++++++++++++++-- apps/permission/models.py | 17 ++- apps/permission/views.py | 11 ++ apps/treasury/forms.py | 4 +- apps/treasury/views.py | 40 ++++--- templates/note/noteactivity_detail.html | 9 +- templates/note/noteactivity_list.html | 8 +- 13 files changed, 272 insertions(+), 105 deletions(-) create mode 100644 apps/permission/views.py diff --git a/apps/activity/views.py b/apps/activity/views.py index feb7591d..51e2ebf5 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -1,5 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later + from datetime import datetime, timezone from django.contrib.auth.mixins import LoginRequiredMixin @@ -11,13 +12,14 @@ from django.utils.translation import gettext_lazy as _ from django_tables2.views import SingleTableView from note.models import NoteUser, Alias, NoteSpecial from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import ActivityForm, GuestForm from .models import Activity, Guest, Entry from .tables import ActivityTable, GuestTable, EntryTable -class ActivityCreateView(LoginRequiredMixin, CreateView): +class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Activity form_class = ActivityForm @@ -30,13 +32,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) -class ActivityListView(LoginRequiredMixin, SingleTableView): +class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = Activity table_class = ActivityTable def get_queryset(self): - return super().get_queryset()\ - .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse() + return super().get_queryset().reverse() def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -50,7 +51,7 @@ class ActivityListView(LoginRequiredMixin, SingleTableView): return ctx -class ActivityDetailView(LoginRequiredMixin, DetailView): +class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = Activity context_object_name = "activity" @@ -66,7 +67,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView): return ctx -class ActivityUpdateView(LoginRequiredMixin, UpdateView): +class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Activity form_class = ActivityForm @@ -74,18 +75,20 @@ class ActivityUpdateView(LoginRequiredMixin, UpdateView): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) -class ActivityInviteView(LoginRequiredMixin, CreateView): +class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Guest form_class = GuestForm template_name = "activity/activity_invite.html" def get_form(self, form_class=None): form = super().get_form(form_class) - form.activity = Activity.objects.get(pk=self.kwargs["pk"]) + form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ + .get(pk=self.kwargs["pk"]) return form def form_valid(self, form): - form.instance.activity = Activity.objects.get(pk=self.kwargs["pk"]) + form.instance.activity = Activity.objects\ + .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"]) return super().form_valid(form) def get_success_url(self, **kwargs): @@ -98,7 +101,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - activity = Activity.objects.get(pk=self.kwargs["pk"]) + activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ + .get(pk=self.kwargs["pk"]) ctx["activity"] = activity matched = [] diff --git a/apps/member/forms.py b/apps/member/forms.py index d731c10c..9c25f2c2 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -49,7 +49,13 @@ class ClubForm(forms.ModelForm): model = Club fields = '__all__' widgets = { - "membership_fee": AmountInput() + "membership_fee": AmountInput(), + "parent_club": Autocomplete( + Club, + attrs={ + 'api_url': '/api/members/club/', + } + ), } diff --git a/apps/member/views.py b/apps/member/views.py index e8bde67d..932899ff 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -22,6 +22,7 @@ from note.models.notes import NoteActivity from note.models.transactions import Transaction from note.tables import HistoryTable, AliasTable, NoteActivityTable from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .filters import UserFilter, UserFilterFormHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ @@ -64,7 +65,7 @@ class UserCreateView(CreateView): return super().form_valid(form) -class UserUpdateView(LoginRequiredMixin, UpdateView): +class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = User fields = ['first_name', 'last_name', 'username', 'email'] template_name = 'member/profile_update.html' @@ -98,7 +99,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): if form.is_valid() and profile_form.is_valid(): new_username = form.data['username'] alias = Alias.objects.filter(name=new_username) - # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer + # Si le nouveau pseudo n'est pas un de nos alias, + # on supprime éventuellement un alias similaire pour le remplacer if not alias.exists(): similar = Alias.objects.filter( normalized_name=Alias.normalize(new_username)) @@ -120,7 +122,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): return reverse_lazy('member:user_detail', args=(self.object.id,)) -class UserDetailView(LoginRequiredMixin, DetailView): +class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ Affiche les informations sur un utilisateur, sa note, ses clubs... """ @@ -128,9 +130,6 @@ 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'] @@ -138,13 +137,13 @@ class UserDetailView(LoginRequiredMixin, DetailView): Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) context['history_list'] = HistoryTable(history_list) - club_list = \ - Membership.objects.all().filter(user=user).only("club") + club_list = Membership.objects.all().filter(user=user)\ + .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).only("club") context['club_list'] = ClubTable(club_list) return context -class UserListView(LoginRequiredMixin, SingleTableView): +class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Affiche la liste des utilisateurs, avec une fonction de recherche statique """ @@ -155,7 +154,7 @@ class UserListView(LoginRequiredMixin, SingleTableView): formhelper_class = UserFilterFormHelper def get_queryset(self, **kwargs): - qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) + qs = super().get_queryset() self.filter = self.filter_class(self.request.GET, queryset=qs) self.filter.form.helper = self.formhelper_class() return self.filter.qs @@ -166,7 +165,7 @@ class UserListView(LoginRequiredMixin, SingleTableView): return context -class ProfileAliasView(LoginRequiredMixin, DetailView): +class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' @@ -178,7 +177,7 @@ class ProfileAliasView(LoginRequiredMixin, DetailView): return context -class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): +class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): form_class = ImageForm def get_context_data(self, **kwargs): @@ -239,8 +238,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): template_name = "member/manage_auth_tokens.html" def get(self, request, *args, **kwargs): - if 'regenerate' in request.GET and Token.objects.filter( - user=request.user).exists(): + if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): Token.objects.get(user=self.request.user).delete() return redirect(reverse_lazy('member:auth_token') + "?show", permanent=True) @@ -249,8 +247,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['token'] = Token.objects.get_or_create( - user=self.request.user)[0] + context['token'] = Token.objects.get_or_create(user=self.request.user)[0] return context @@ -259,7 +256,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): # ******************************* # -class ClubCreateView(LoginRequiredMixin, CreateView): +class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Club """ @@ -271,38 +268,32 @@ class ClubCreateView(LoginRequiredMixin, CreateView): return super().form_valid(form) -class ClubListView(LoginRequiredMixin, SingleTableView): +class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Clubs """ 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): +class ClubDetailView(ProtectQuerysetMixin, 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"] club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id') context['history_list'] = HistoryTable(club_transactions) - club_member = \ - Membership.objects.all().filter(club=club) + club_member = Membership.objects.filter(club=club)\ + .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).all() # TODO: consider only valid Membership context['member_list'] = club_member return context -class ClubAliasView(LoginRequiredMixin, DetailView): +class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = Club template_name = 'member/club_alias.html' context_object_name = 'club' @@ -314,7 +305,7 @@ class ClubAliasView(LoginRequiredMixin, DetailView): return context -class ClubUpdateView(LoginRequiredMixin, UpdateView): +class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Club context_object_name = "club" form_class = ClubForm @@ -333,7 +324,7 @@ class ClubPictureUpdateView(PictureUpdateView): return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id}) -class ClubAddMemberView(LoginRequiredMixin, CreateView): +class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Membership form_class = MembershipForm template_name = 'member/add_members.html' @@ -344,7 +335,8 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): "change")) def get_context_data(self, **kwargs): - club = Club.objects.get(pk=self.kwargs["pk"]) + club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ + .get(pk=self.kwargs["pk"]) context = super().get_context_data(**kwargs) context['formset'] = MemberFormSet() context['helper'] = FormSetHelper() @@ -367,36 +359,40 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): return super().form_valid(formset) -class ClubLinkedNotesView(LoginRequiredMixin, SingleTableView): +class ClubLinkedNotesView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = NoteActivity table_class = NoteActivityTable def get_queryset(self): - return super().get_queryset().filter(club=self.get_object())\ - .filter(PermissionBackend.filter_queryset(self.request.user, NoteActivity, "view")) + return super().get_queryset().filter(club=self.get_object()) def get_object(self): if hasattr(self, 'object'): return self.object - self.object = Club.objects.get(pk=int(self.kwargs["pk"])) + self.object = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ + .get(pk=int(self.kwargs["pk"])) return self.object def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["object"] = ctx["club"] = self.get_object() + club = ctx["object"] = ctx["club"] = self.get_object() + + empty_note = NoteActivity(note_name="", club=club, controller=self.request.user) + ctx["can_create"] = PermissionBackend().has_perm(self.request.user, "note.add_noteactivity", empty_note) return ctx -class ClubLinkedNoteCreateView(LoginRequiredMixin, CreateView): +class ClubLinkedNoteCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = NoteActivity form_class = NoteActivityForm def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - club = Club.objects.get(pk=self.kwargs["club_pk"]) + club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ + .get(pk=self.kwargs["club_pk"]) ctx["object"] = ctx["club"] = club ctx["form"].fields["club"].initial = club @@ -408,14 +404,15 @@ class ClubLinkedNoteCreateView(LoginRequiredMixin, CreateView): kwargs={"club_pk": self.object.club.pk, "pk": self.object.pk}) -class ClubLinkedNoteUpdateView(LoginRequiredMixin, UpdateView): +class ClubLinkedNoteUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = NoteActivity form_class = NoteActivityForm def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["club"] = Club.objects.get(pk=self.kwargs["club_pk"]) + ctx["club"] = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ + .get(pk=self.kwargs["club_pk"]) return ctx @@ -424,15 +421,15 @@ class ClubLinkedNoteUpdateView(LoginRequiredMixin, UpdateView): kwargs={"club_pk": self.object.club.pk, "pk": self.object.pk}) -class ClubLinkedNoteDetailView(LoginRequiredMixin, DetailView): +class ClubLinkedNoteDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = NoteActivity def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - note = NoteActivity.objects.get(pk=self.kwargs["pk"]) + note = self.get_queryset().filter(pk=self.kwargs["pk"]).get() - transactions = Transaction.objects.all().filter(Q(source=note) | Q(destination=note))\ + transactions = Transaction.objects.filter(Q(source=note) | Q(destination=note))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by("-id") ctx['history_list'] = HistoryTable(transactions) ctx["note"] = note diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index e9c8a0a9..dd024963 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -1,13 +1,13 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser +from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, NoteActivity from .transactions import MembershipTransaction, Transaction, \ TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction __all__ = [ # Notes - 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', + 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', 'NoteActivity', # Transactions 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', 'RecurrentTransaction', 'SpecialTransaction', diff --git a/apps/note/views.py b/apps/note/views.py index 25279281..ac9b3e40 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -9,6 +9,7 @@ from django_tables2 import SingleTableView from django.urls import reverse_lazy from note_kfet.inputs import AmountInput from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import TransactionTemplateForm from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial @@ -16,7 +17,7 @@ from .models.transactions import SpecialTransaction from .tables import HistoryTable, ButtonTable -class TransactionCreateView(LoginRequiredMixin, SingleTableView): +class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`. e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial` @@ -26,12 +27,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): model = Transaction # 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_queryset(self, **kwargs): + return super().get_queryset(**kwargs).order_by("-id").all()[:50] def get_context_data(self, **kwargs): """ @@ -42,12 +40,14 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): context['amount_widget'] = AmountInput(attrs={"id": "amount"}) context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk - context['special_types'] = NoteSpecial.objects.order_by("special_type").all() + context['special_types'] = NoteSpecial.objects\ + .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\ + .order_by("special_type").all() return context -class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): +class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create TransactionTemplate """ @@ -56,7 +56,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): success_url = reverse_lazy('note:template_list') -class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): +class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List TransactionsTemplates """ @@ -64,7 +64,7 @@ class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): table_class = ButtonTable -class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): +class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ """ model = TransactionTemplate @@ -72,21 +72,19 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): success_url = reverse_lazy('note:template_list') -class ConsoView(LoginRequiredMixin, SingleTableView): +class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ The Magic View that make people pay their beer and burgers. (Most of the magic happens in the dark world of Javascript see consos.js) """ + model = Transaction 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_queryset(self, **kwargs): + return super().get_queryset(**kwargs).order_by("-id").all()[:50] def get_context_data(self, **kwargs): """ diff --git a/apps/permission/backends.py b/apps/permission/backends.py index e61b0719..f838fc1f 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -5,7 +5,7 @@ from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, AnonymousUser from django.contrib.contenttypes.models import ContentType from django.db.models import Q, F -from note.models import Note, NoteUser, NoteClub, NoteSpecial +from note.models import Note, NoteUser, NoteClub, NoteSpecial, NoteActivity from note_kfet.middlewares import get_current_session from member.models import Membership, Club @@ -35,7 +35,7 @@ class PermissionBackend(ModelBackend): model__app_label=model.app_label, # For polymorphic models, we don't filter on model type type=type, ).all(): - if not isinstance(model, permission.model.__class__): + if not isinstance(model, permission.model.__class__) or not permission.club: continue club = Club.objects.get(pk=permission.club) @@ -49,6 +49,7 @@ class PermissionBackend(ModelBackend): NoteUser=NoteUser, NoteClub=NoteClub, NoteSpecial=NoteSpecial, + NoteActivity=NoteActivity, F=F, Q=Q ) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 31b59069..e0430530 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -176,7 +176,7 @@ "note", "alias" ], - "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", + "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}, {\"note__in\": [\"NoteActivity\", \"objects\", [\"all\"]]}]", "type": "view", "mask": 1, "field": "", @@ -386,7 +386,7 @@ "note", "transaction" ], - "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", + "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", "type": "add", "mask": 2, "field": "", @@ -783,6 +783,111 @@ "description": "Validate invitation transactions" } }, + { + "model": "permission.permission", + "pk": 47, + "fields": { + "model": [ + "member", + "club" + ], + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Update club" + } + }, + { + "model": "permission.permission", + "pk": 48, + "fields": { + "model": [ + "note", + "noteactivity" + ], + "query": "{\"club\": [\"club\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Manage notes that are linked to a club" + } + }, + { + "model": "permission.permission", + "pk": 49, + "fields": { + "model": [ + "note", + "noteactivity" + ], + "query": "{\"club\": [\"club\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View notes that are linked to a club" + } + }, + { + "model": "permission.permission", + "pk": 50, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"AND\", [\"OR\", {\"source__noteactivity__controller\": [\"user\"]}, {\"destination__noteactivity__controller\": [\"user\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", + "type": "add", + "mask": 2, + "field": "", + "description": "Add transactions linked to a noteactivity" + } + }, + { + "model": "permission.permission", + "pk": 51, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"AND\", [\"OR\", {\"source__noteactivity__controller\": [\"user\"]}, {\"destination__noteactivity__controller\": [\"user\"]}]]", + "type": "view", + "mask": 1, + "field": "", + "description": "View transactions linked to a noteactivity" + } + }, + { + "model": "permission.permission", + "pk": 52, + "fields": { + "model": [ + "note", + "note" + ], + "query": "{\"noteactivity__controller\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View note activity" + } + }, + { + "model": "permission.permission", + "pk": 53, + "fields": { + "model": [ + "note", + "noteactivity" + ], + "query": "{\"controller\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View note activity" + } + }, { "model": "permission.rolepermissions", "pk": 1, @@ -810,7 +915,6 @@ 3, 4, 5, - 6, 7, 8, 9, @@ -827,7 +931,12 @@ 35, 36, 39, - 40 + 40, + 6, + 52, + 53, + 51, + 50 ] } }, @@ -838,9 +947,9 @@ "role": 8, "permissions": [ 19, - 20, 21, - 22 + 22, + 20 ] } }, @@ -880,5 +989,18 @@ 46 ] } + }, + { + "model": "permission.rolepermissions", + "pk": 6, + "fields": { + "role": 7, + "permissions": [ + 22, + 47, + 48, + 49 + ] + } } ] diff --git a/apps/permission/models.py b/apps/permission/models.py index 205f5b41..d1b55090 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -38,20 +38,29 @@ class InstancedPermission: if permission_type == self.type: self.update_query() - # Don't increase indexes - obj.pk = 0 + # Don't increase indexes, if the primary key is an AutoField + if not hasattr(obj, "pk") or not obj.pk: + obj.pk = 0 + oldpk = None + else: + oldpk = obj.pk + # Ensure previous models are deleted + self.model.model_class().objects.filter(pk=obj.pk).delete() # Force insertion, no data verification, no trigger Model.save(obj, force_insert=True) - ret = obj in self.model.model_class().objects.filter(self.query).all() + ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() # Delete testing object Model.delete(obj) + + # If the primary key was specified, we restore it + obj.pk = oldpk return ret if permission_type == self.type: if self.field and field_name != self.field: return False self.update_query() - return obj in self.model.model_class().objects.filter(self.query).all() + return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() else: return False diff --git a/apps/permission/views.py b/apps/permission/views.py new file mode 100644 index 00000000..bbd9872f --- /dev/null +++ b/apps/permission/views.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from permission.backends import PermissionBackend + + +class ProtectQuerysetMixin: + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + + return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index 7fe7de4c..ad479e14 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -8,6 +8,7 @@ from crispy_forms.layout import Submit from django import forms from django.utils.translation import gettext_lazy as _ from note_kfet.inputs import DatePickerInput, AmountInput +from permission.backends import PermissionBackend from .models import Invoice, Product, Remittance, SpecialTransactionProxy @@ -131,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm): # Add submit button self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) - self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) + self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\ + .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) def clean_last_name(self): """ diff --git a/apps/treasury/views.py b/apps/treasury/views.py index c374ced1..adf38aaa 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView from django_tables2 import SingleTableView from note.models import SpecialTransaction, NoteSpecial from note_kfet.settings.base import BASE_DIR +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm from .models import Invoice, Product, Remittance, SpecialTransactionProxy from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable -class InvoiceCreateView(LoginRequiredMixin, CreateView): +class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Invoice """ @@ -67,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView): return reverse_lazy('treasury:invoice_list') -class InvoiceListView(LoginRequiredMixin, SingleTableView): +class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Invoices """ @@ -75,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView): table_class = InvoiceTable -class InvoiceUpdateView(LoginRequiredMixin, UpdateView): +class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Create Invoice """ @@ -130,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): def get(self, request, **kwargs): pk = kwargs["pk"] - invoice = Invoice.objects.get(pk=pk) + invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk) products = Product.objects.filter(invoice=invoice).all() # Informations of the BDE. Should be updated when the school will move. @@ -188,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): return response -class RemittanceCreateView(LoginRequiredMixin, CreateView): +class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Remittance """ @@ -201,7 +203,9 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["table"] = RemittanceTable(data=Remittance.objects.all()) + ctx["table"] = RemittanceTable(data=Remittance.objects + .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) + .all()) ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) return ctx @@ -216,22 +220,28 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) - ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) + ctx["opened_remittances"] = RemittanceTable( + data=Remittance.objects.filter(closed=False).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) + ctx["closed_remittances"] = RemittanceTable( + data=Remittance.objects.filter(closed=True).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all()) ctx["special_transactions_no_remittance"] = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), - specialtransactionproxy__remittance=None).all(), + specialtransactionproxy__remittance=None).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), exclude=('remittance_remove', )) ctx["special_transactions_with_remittance"] = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), - specialtransactionproxy__remittance__closed=False).all(), + specialtransactionproxy__remittance__closed=False).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), exclude=('remittance_add', )) return ctx -class RemittanceUpdateView(LoginRequiredMixin, UpdateView): +class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Update Remittance """ @@ -244,8 +254,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["table"] = RemittanceTable(data=Remittance.objects.all()) - data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all() + ctx["table"] = RemittanceTable(data=Remittance.objects.filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) + data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all() ctx["special_transactions"] = SpecialTransactionTable( data=data, exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) @@ -253,7 +265,7 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView): return ctx -class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): +class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Attach a special transaction to a remittance """ diff --git a/templates/note/noteactivity_detail.html b/templates/note/noteactivity_detail.html index 731707f6..aab9e055 100644 --- a/templates/note/noteactivity_detail.html +++ b/templates/note/noteactivity_detail.html @@ -3,6 +3,7 @@ {% load i18n %} {% load render_table from django_tables2 %} {% load pretty_money %} +{% load perms %} {% block profile_info %} {% include "member/club_info.html" %} @@ -25,9 +26,11 @@
{{ note.balance|pretty_money }}
- + {% if "change_"|has_perm:note %} + + {% endif %} diff --git a/templates/note/noteactivity_list.html b/templates/note/noteactivity_list.html index b4d69536..3cd5b786 100644 --- a/templates/note/noteactivity_list.html +++ b/templates/note/noteactivity_list.html @@ -19,9 +19,11 @@ - - - + {% if can_create %} + + + + {% endif %} {% endblock %}